diff --git a/UseLocalResources.ps1 b/UseLocalResources.ps1
new file mode 100644
index 0000000000..f9d9df01bb
--- /dev/null
+++ b/UseLocalResources.ps1
@@ -0,0 +1,12 @@
+$CSPROJ="osu.Game/osu.Game.csproj"
+$SLN="osu.sln"
+
+dotnet remove $CSPROJ package ppy.osu.Game.Resources;
+dotnet sln $SLN add ../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj
+dotnet add $CSPROJ reference ../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj
+
+$SLNF=Get-Content "osu.Desktop.slnf" | ConvertFrom-Json
+$TMP=New-TemporaryFile
+$SLNF.solution.projects += ("../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj")
+ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8
+Move-Item -Path $TMP -Destination "osu.Desktop.slnf" -Force
diff --git a/UseLocalResources.sh b/UseLocalResources.sh
new file mode 100755
index 0000000000..6d9d2b6016
--- /dev/null
+++ b/UseLocalResources.sh
@@ -0,0 +1,11 @@
+CSPROJ="osu.Game/osu.Game.csproj"
+SLN="osu.sln"
+
+dotnet remove $CSPROJ package ppy.osu.Game.Resources;
+dotnet sln $SLN add ../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj
+dotnet add $CSPROJ reference ../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj
+
+SLNF="osu.Desktop.slnf"
+TMP=$(mktemp)
+jq '.solution.projects += ["../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj"]' $SLNF > $TMP
+mv -f $TMP $SLNF
diff --git a/osu.Android.props b/osu.Android.props
index 8237a570ff..5f3fb858ee 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index 5fa4e24f5e..13ea46eadf 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("player score matching expected bonus score", () =>
{
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
- double totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
+ long totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult;
});
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
index 94655f3cf7..c7e3516d62 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -248,6 +248,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
break;
}
+ slider.Path.ExpectedDistance.Value = null;
piece.ControlPoint.Type = type;
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs
index bea5d4f5d9..0a1aab9ef1 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs
@@ -56,6 +56,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{
switch (nested)
{
+ //Freezing the SliderTicks doesnt play well with snaking sliders
+ case SliderTick:
//SliderRepeat wont layer correctly if preempt is changed.
case SliderRepeat:
break;
diff --git a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
index 46c8e7c02a..c19ed3fb35 100644
--- a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
@@ -49,7 +49,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
private const float max_rotation = 0.25f;
public IShader? TextureShader { get; private set; }
- public IShader? RoundedTextureShader { get; private set; }
protected Texture? Texture { get; set; }
@@ -69,7 +68,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
[BackgroundDependencyLoader]
private void load(ShaderManager shaders)
{
- RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
}
@@ -247,18 +245,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
texture ??= renderer.WhitePixel;
RectangleF textureRect = texture.GetTextureRect();
- var shader = GetAppropriateShader(renderer);
-
renderer.SetBlend(BlendingParameters.Additive);
renderer.PushLocalMatrix(DrawInfo.Matrix);
- shader.Bind();
+ TextureShader.Bind();
texture.Bind();
for (int i = 0; i < points.Count; i++)
drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex);
- shader.Unbind();
+ TextureShader.Unbind();
renderer.PopLocalMatrix();
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs
index ad6f08dbd4..a4aa0e1fad 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
var cont = new Container
{
RelativeSizeAxes = Axes.Both,
- Height = 0.8f,
+ Height = 0.2f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
@@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
var cont = new Container
{
RelativeSizeAxes = Axes.Both,
- Height = 0.8f,
+ Height = 0.2f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
@@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
var barLine = new BarLine
{
Major = major,
- StartTime = Time.Current + 2000,
+ StartTime = Time.Current + 5000,
};
var cpi = new ControlPointInfo();
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs
new file mode 100644
index 0000000000..b8c0f6f11e
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs
@@ -0,0 +1,39 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ [TestFixture]
+ public class TestSceneDrawableSwell : TaikoSkinnableTestScene
+ {
+ [Test]
+ public void TestHits()
+ {
+ AddStep("Centre hit", () => SetContents(_ => new DrawableSwell(createHitAtCurrentTime())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }));
+ }
+
+ private Swell createHitAtCurrentTime()
+ {
+ var hit = new Swell
+ {
+ StartTime = Time.Current + 3000,
+ EndTime = Time.Current + 6000,
+ };
+
+ hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ return hit;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
index 52d24b567f..eff9f58751 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
@@ -4,6 +4,7 @@
#nullable disable
using System;
+using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
@@ -25,11 +26,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
TimeRange = { Value = 5000 },
};
- public TestSceneTaikoPlayfield()
+ [SetUpSteps]
+ public void SetUpSteps()
{
TaikoBeatmap beatmap;
- bool kiai = false;
-
AddStep("set beatmap", () =>
{
Beatmap.Value = CreateWorkingBeatmap(beatmap = new TaikoBeatmap());
@@ -41,12 +41,28 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
AddStep("Load playfield", () => SetContents(_ => new TaikoPlayfield
{
+ Height = 0.2f,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
- Height = 0.6f,
}));
+ }
+ [Test]
+ public void TestBasic()
+ {
+ AddStep("do nothing", () => { });
+ }
+
+ [Test]
+ public void TestHeightChanges()
+ {
AddRepeatStep("change height", () => this.ChildrenOfType().ForEach(p => p.Height = Math.Max(0.2f, (p.Height + 0.2f) % 1f)), 50);
+ }
+
+ [Test]
+ public void TestKiai()
+ {
+ bool kiai = false;
AddStep("Toggle kiai", () =>
{
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
index 4aed8212e8..b01adc880a 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
@@ -74,7 +74,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
switch (state)
{
case ArmedState.Hit:
- this.ScaleTo(0, 100, Easing.OutQuint);
+ this.ScaleTo(1.4f, 200, Easing.OutQuint);
+ this.FadeOut(200, Easing.OutQuint);
break;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
index c0c80eaa4a..400c2f40b1 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
@@ -133,6 +133,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override void OnApply()
{
base.OnApply();
+
+ // TODO: THIS CANNOT BE HERE, it makes pooling pointless (see https://github.com/ppy/osu/issues/21072).
RecreatePieces();
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonBarLine.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonBarLine.cs
new file mode 100644
index 0000000000..402e88b64d
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonBarLine.cs
@@ -0,0 +1,83 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
+using osuTK;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Argon
+{
+ public class ArgonBarLine : CompositeDrawable
+ {
+ private Container majorEdgeContainer = null!;
+
+ private Bindable major = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableHitObject)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ const float line_offset = 8;
+ var majorPieceSize = new Vector2(6, 20);
+
+ InternalChildren = new Drawable[]
+ {
+ line = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ EdgeSmoothness = new Vector2(0.5f, 0),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ majorEdgeContainer = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ new Circle
+ {
+ Name = "Top line",
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.BottomCentre,
+ Size = majorPieceSize,
+ Y = -line_offset,
+ },
+ new Circle
+ {
+ Name = "Bottom line",
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.TopCentre,
+ Size = majorPieceSize,
+ Y = line_offset,
+ },
+ }
+ }
+ };
+
+ major = ((DrawableBarLine)drawableHitObject).Major.GetBoundCopy();
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ major.BindValueChanged(updateMajor, true);
+ }
+
+ private Box line = null!;
+
+ private void updateMajor(ValueChangedEvent major)
+ {
+ line.Alpha = major.NewValue ? 1f : 0.5f;
+ line.Width = major.NewValue ? 1 : 0.5f;
+ majorEdgeContainer.Alpha = major.NewValue ? 1 : 0;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCentreCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCentreCirclePiece.cs
new file mode 100644
index 0000000000..551a5af078
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCentreCirclePiece.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Sprites;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Argon
+{
+ public class ArgonCentreCirclePiece : ArgonCirclePiece
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AccentColour = ColourInfo.GradientVertical(
+ new Color4(241, 0, 0, 255),
+ new Color4(167, 0, 0, 255)
+ );
+
+ AddInternal(new SpriteIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Icon = FontAwesome.Solid.AngleLeft,
+ Size = new Vector2(ICON_SIZE),
+ Scale = new Vector2(0.8f, 1)
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs
new file mode 100644
index 0000000000..c22c0e9e79
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs
@@ -0,0 +1,116 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Audio.Track;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.Objects;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Argon
+{
+ public abstract class ArgonCirclePiece : BeatSyncedContainer
+ {
+ public const float ICON_SIZE = 20 / 70f;
+
+ private const double pre_beat_transition_time = 80;
+
+ private const float flash_opacity = 0.3f;
+
+ private ColourInfo accentColour;
+
+ ///
+ /// The colour of the inner circle and outer glows.
+ ///
+ public ColourInfo AccentColour
+ {
+ get => accentColour;
+ set
+ {
+ accentColour = value;
+
+ ring.Colour = AccentColour.MultiplyAlpha(0.5f);
+ ring2.Colour = AccentColour;
+ }
+ }
+
+ [Resolved]
+ private DrawableHitObject drawableHitObject { get; set; } = null!;
+
+ private readonly Drawable flash;
+
+ private readonly RingPiece ring;
+ private readonly RingPiece ring2;
+
+ protected ArgonCirclePiece()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ EarlyActivationMilliseconds = pre_beat_transition_time;
+
+ AddRangeInternal(new[]
+ {
+ new Circle
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = new Color4(0, 0, 0, 190)
+ },
+ ring = new RingPiece(20 / 70f),
+ ring2 = new RingPiece(5 / 70f),
+ flash = new Circle
+ {
+ Name = "Flash layer",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
+ Alpha = 0,
+ },
+ });
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ drawableHitObject.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(drawableHitObject, drawableHitObject.State.Value);
+ }
+
+ private void updateStateTransforms(DrawableHitObject h, ArmedState state)
+ {
+ if (h.HitObject is not Hit)
+ return;
+
+ switch (state)
+ {
+ case ArmedState.Hit:
+ using (BeginAbsoluteSequence(h.HitStateUpdateTime))
+ {
+ flash.FadeTo(0.9f).FadeOut(500, Easing.OutQuint);
+ }
+
+ break;
+ }
+ }
+
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
+ {
+ if (!effectPoint.KiaiMode)
+ return;
+
+ if (drawableHitObject.State.Value == ArmedState.Idle)
+ {
+ flash
+ .FadeTo(flash_opacity)
+ .Then()
+ .FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonElongatedCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonElongatedCirclePiece.cs
new file mode 100644
index 0000000000..f86f181b2e
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonElongatedCirclePiece.cs
@@ -0,0 +1,33 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Argon
+{
+ public class ArgonElongatedCirclePiece : ArgonCirclePiece
+ {
+ public ArgonElongatedCirclePiece()
+ {
+ RelativeSizeAxes = Axes.Y;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AccentColour = ColourInfo.GradientVertical(
+ new Color4(241, 161, 0, 255),
+ new Color4(167, 111, 0, 255)
+ );
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ Width = Parent.DrawSize.X + DrawHeight;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitExplosion.cs
new file mode 100644
index 0000000000..05bb9bcb9a
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitExplosion.cs
@@ -0,0 +1,87 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.UI;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Argon
+{
+ public class ArgonHitExplosion : CompositeDrawable, IAnimatableHitExplosion
+ {
+ private readonly TaikoSkinComponents component;
+ private readonly Circle outer;
+
+ public ArgonHitExplosion(TaikoSkinComponents component)
+ {
+ this.component = component;
+
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ outer = new Circle
+ {
+ Name = "Outer circle",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Colour = ColourInfo.GradientVertical(
+ new Color4(255, 227, 236, 255),
+ new Color4(255, 198, 211, 255)
+ ),
+ Masking = true,
+ },
+ new Circle
+ {
+ Name = "Inner circle",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.White,
+ Size = new Vector2(0.85f),
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = new Color4(255, 132, 191, 255).Opacity(0.5f),
+ Radius = 45,
+ },
+ Masking = true,
+ },
+ };
+ }
+
+ public void Animate(DrawableHitObject drawableHitObject)
+ {
+ this.FadeOut();
+
+ switch (component)
+ {
+ case TaikoSkinComponents.TaikoExplosionGreat:
+ this.FadeIn(30, Easing.In)
+ .Then()
+ .FadeOut(450, Easing.OutQuint);
+ break;
+
+ case TaikoSkinComponents.TaikoExplosionOk:
+ this.FadeTo(0.2f, 30, Easing.In)
+ .Then()
+ .FadeOut(200, Easing.OutQuint);
+ break;
+ }
+ }
+
+ public void AnimateSecondHit()
+ {
+ outer.ResizeTo(new Vector2(TaikoStrongableHitObject.STRONG_SCALE), 500, Easing.OutQuint);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitTarget.cs
new file mode 100644
index 0000000000..ec2eccd595
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitTarget.cs
@@ -0,0 +1,72 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Taiko.Objects;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Argon
+{
+ public class ArgonHitTarget : CompositeDrawable
+ {
+ ///
+ /// Thickness of all drawn line pieces.
+ ///
+ public ArgonHitTarget()
+ {
+ RelativeSizeAxes = Axes.Both;
+ Masking = true;
+
+ const float border_thickness = 4f;
+
+ InternalChildren = new Drawable[]
+ {
+ new Circle
+ {
+ Name = "Bar Upper",
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Y = -border_thickness,
+ RelativeSizeAxes = Axes.Y,
+ Size = new Vector2(border_thickness, (1 - TaikoStrongableHitObject.DEFAULT_STRONG_SIZE)),
+ },
+ new Circle
+ {
+ Name = "Outer circle",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.White,
+ Blending = BlendingParameters.Additive,
+ Alpha = 0.1f,
+ Size = new Vector2(TaikoHitObject.DEFAULT_SIZE),
+ Masking = true,
+ },
+ new Circle
+ {
+ Name = "Inner circle",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.White,
+ Blending = BlendingParameters.Additive,
+ Alpha = 0.1f,
+ Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * 0.85f),
+ Masking = true,
+ },
+ new Circle
+ {
+ Name = "Bar Lower",
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ RelativeSizeAxes = Axes.Y,
+ Y = border_thickness,
+ Size = new Vector2(border_thickness, (1 - TaikoStrongableHitObject.DEFAULT_STRONG_SIZE)),
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonInputDrum.cs
new file mode 100644
index 0000000000..528e75aabb
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonInputDrum.cs
@@ -0,0 +1,218 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Screens.Ranking;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Argon
+{
+ public class ArgonInputDrum : AspectContainer
+ {
+ private const float rim_size = 0.3f;
+
+ public ArgonInputDrum()
+ {
+ RelativeSizeAxes = Axes.Y;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ const float middle_split = 6;
+
+ InternalChild = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Scale = new Vector2(0.9f),
+ Children = new Drawable[]
+ {
+ new TaikoHalfDrum(false)
+ {
+ Name = "Left Half",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.CentreRight,
+ RelativeSizeAxes = Axes.Both,
+ RimAction = TaikoAction.LeftRim,
+ CentreAction = TaikoAction.LeftCentre
+ },
+ new TaikoHalfDrum(true)
+ {
+ Name = "Right Half",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.CentreLeft,
+ RelativeSizeAxes = Axes.Both,
+ RimAction = TaikoAction.RightRim,
+ CentreAction = TaikoAction.RightCentre
+ },
+ new CircularContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Masking = true,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = OsuColour.Gray(38 / 255f),
+ Width = middle_split,
+ RelativeSizeAxes = Axes.Y,
+ },
+ new Box
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = OsuColour.Gray(48 / 255f),
+ Width = middle_split,
+ Height = 1 - rim_size,
+ RelativeSizeAxes = Axes.Y,
+ },
+ },
+ }
+ }
+ };
+ }
+
+ ///
+ /// A half-drum. Contains one centre and one rim hit.
+ ///
+ private class TaikoHalfDrum : CompositeDrawable, IKeyBindingHandler
+ {
+ ///
+ /// The key to be used for the rim of the half-drum.
+ ///
+ public TaikoAction RimAction;
+
+ ///
+ /// The key to be used for the centre of the half-drum.
+ ///
+ public TaikoAction CentreAction;
+
+ private readonly Drawable rimHit;
+ private readonly Drawable centreHit;
+
+ public TaikoHalfDrum(bool flipped)
+ {
+ Anchor anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight;
+
+ Masking = true;
+
+ Anchor = anchor;
+ Origin = anchor;
+
+ RelativeSizeAxes = Axes.Both;
+ // Extend maskable region for glow.
+ Height = 2f;
+
+ InternalChildren = new Drawable[]
+ {
+ new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.5f,
+ Children = new[]
+ {
+ new Circle
+ {
+ Anchor = anchor,
+ Colour = OsuColour.Gray(51 / 255f),
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both
+ },
+ rimHit = new Circle
+ {
+ Anchor = anchor,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Colour = ColourInfo.GradientHorizontal(
+ new Color4(227, 248, 255, 255),
+ new Color4(198, 245, 255, 255)
+ ),
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = new Color4(126, 215, 253, 170),
+ Radius = 50,
+ },
+ Alpha = 0,
+ },
+ new Circle
+ {
+ Anchor = anchor,
+ Origin = Anchor.Centre,
+ Colour = OsuColour.Gray(64 / 255f),
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(1 - rim_size)
+ },
+ centreHit = new Circle
+ {
+ Anchor = anchor,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Colour = ColourInfo.GradientHorizontal(
+ new Color4(255, 227, 236, 255),
+ new Color4(255, 198, 211, 255)
+ ),
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = new Color4(255, 147, 199, 255),
+ Radius = 50,
+ },
+ Size = new Vector2(1 - rim_size),
+ Alpha = 0,
+ }
+ },
+ },
+ };
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ Drawable? target = null;
+
+ if (e.Action == CentreAction)
+ target = centreHit;
+ else if (e.Action == RimAction)
+ target = rimHit;
+
+ if (target != null)
+ {
+ const float alpha_amount = 0.5f;
+
+ const float down_time = 40;
+ const float up_time = 750;
+
+ target.Animate(
+ t => t.FadeTo(Math.Min(target.Alpha + alpha_amount, 1), down_time, Easing.OutQuint)
+ ).Then(
+ t => t.FadeOut(up_time, Easing.OutQuint)
+ );
+ }
+
+ return false;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs
new file mode 100644
index 0000000000..baaf9e41e2
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs
@@ -0,0 +1,198 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Argon
+{
+ public class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
+ {
+ protected readonly HitResult Result;
+
+ protected SpriteText JudgementText { get; private set; } = null!;
+
+ private RingExplosion? ringExplosion;
+
+ [Resolved]
+ private OsuColour colours { get; set; } = null!;
+
+ public ArgonJudgementPiece(HitResult result)
+ {
+ Result = result;
+ RelativePositionAxes = Axes.Both;
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChildren = new Drawable[]
+ {
+ JudgementText = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = Result.GetDescription().ToUpperInvariant(),
+ Colour = colours.ForHitResult(Result),
+ Blending = BlendingParameters.Additive,
+ Spacing = new Vector2(10, 0),
+ RelativePositionAxes = Axes.Both,
+ Font = OsuFont.Default.With(size: 20, weight: FontWeight.Regular),
+ },
+ };
+
+ if (Result.IsHit())
+ {
+ AddInternal(ringExplosion = new RingExplosion(Result)
+ {
+ Colour = colours.ForHitResult(Result),
+ RelativePositionAxes = Axes.Y,
+ });
+ }
+ }
+
+ ///
+ /// Plays the default animation for this judgement piece.
+ ///
+ ///
+ /// The base implementation only handles fade (for all result types) and misses.
+ /// Individual rulesets are recommended to implement their appropriate hit animations.
+ ///
+ public virtual void PlayAnimation()
+ {
+ const double duration = 800;
+
+ switch (Result)
+ {
+ default:
+ JudgementText.MoveToY(-0.6f)
+ .MoveToY(-1.0f, duration, Easing.OutQuint);
+
+ JudgementText
+ .ScaleTo(Vector2.One)
+ .ScaleTo(new Vector2(1.4f), duration, Easing.OutQuint);
+ break;
+
+ case HitResult.Miss:
+ this.ScaleTo(1.6f);
+ this.ScaleTo(1, 100, Easing.In);
+
+ JudgementText.MoveTo(Vector2.Zero);
+ JudgementText.MoveToOffset(new Vector2(0, 100), duration, Easing.InQuint);
+
+ this.RotateTo(0);
+ this.RotateTo(40, duration, Easing.InQuint);
+ break;
+ }
+
+ this.FadeOutFromOne(duration, Easing.OutQuint);
+
+ ringExplosion?.PlayAnimation();
+ }
+
+ public Drawable? GetAboveHitObjectsProxiedContent() => null;
+
+ private class RingExplosion : CompositeDrawable
+ {
+ private readonly float travel = 58;
+
+ public RingExplosion(HitResult result)
+ {
+ const float thickness = 4;
+
+ const float small_size = 9;
+ const float large_size = 14;
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Blending = BlendingParameters.Additive;
+
+ int countSmall = 0;
+ int countLarge = 0;
+
+ switch (result)
+ {
+ case HitResult.Meh:
+ countSmall = 3;
+ travel *= 0.3f;
+ break;
+
+ case HitResult.Ok:
+ case HitResult.Good:
+ countSmall = 4;
+ travel *= 0.6f;
+ break;
+
+ case HitResult.Great:
+ case HitResult.Perfect:
+ countSmall = 4;
+ countLarge = 4;
+ break;
+ }
+
+ for (int i = 0; i < countSmall; i++)
+ AddInternal(new RingPiece(thickness) { Size = new Vector2(small_size) });
+
+ for (int i = 0; i < countLarge; i++)
+ AddInternal(new RingPiece(thickness) { Size = new Vector2(large_size) });
+ }
+
+ public void PlayAnimation()
+ {
+ foreach (var c in InternalChildren)
+ {
+ const float start_position_ratio = 0.6f;
+
+ float direction = RNG.NextSingle(0, 360);
+ float distance = RNG.NextSingle(travel / 2, travel);
+
+ c.MoveTo(new Vector2(
+ MathF.Cos(direction) * distance * start_position_ratio,
+ MathF.Sin(direction) * distance * start_position_ratio
+ ));
+
+ c.MoveTo(new Vector2(
+ MathF.Cos(direction) * distance,
+ MathF.Sin(direction) * distance
+ ), 600, Easing.OutQuint);
+ }
+
+ this.FadeOutFromOne(1000, Easing.OutQuint);
+ }
+
+ public class RingPiece : CircularContainer
+ {
+ public RingPiece(float thickness = 9)
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Masking = true;
+ BorderThickness = thickness;
+ BorderColour = Color4.White;
+
+ Child = new Box
+ {
+ AlwaysPresent = true,
+ Alpha = 0,
+ RelativeSizeAxes = Axes.Both
+ };
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonPlayfieldBackgroundLeft.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonPlayfieldBackgroundLeft.cs
new file mode 100644
index 0000000000..ebde83b607
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonPlayfieldBackgroundLeft.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Argon
+{
+ public class ArgonPlayfieldBackgroundLeft : CompositeDrawable
+ {
+ public ArgonPlayfieldBackgroundLeft()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = Color4.Black,
+ RelativeSizeAxes = Axes.Both,
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonPlayfieldBackgroundRight.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonPlayfieldBackgroundRight.cs
new file mode 100644
index 0000000000..bd0f3ab276
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonPlayfieldBackgroundRight.cs
@@ -0,0 +1,28 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Argon
+{
+ public class ArgonPlayfieldBackgroundRight : CompositeDrawable
+ {
+ public ArgonPlayfieldBackgroundRight()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = Color4.Black,
+ Alpha = 0.7f,
+ RelativeSizeAxes = Axes.Both,
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonRimCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonRimCirclePiece.cs
new file mode 100644
index 0000000000..fd81221be3
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonRimCirclePiece.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Sprites;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Argon
+{
+ public class ArgonRimCirclePiece : ArgonCirclePiece
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AccentColour = ColourInfo.GradientVertical(
+ new Color4(0, 161, 241, 255),
+ new Color4(0, 111, 167, 255)
+ );
+
+ AddInternal(new SpriteIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Icon = FontAwesome.Solid.AngleLeft,
+ Size = new Vector2(ICON_SIZE),
+ Scale = new Vector2(0.8f, 1)
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwellCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwellCirclePiece.cs
new file mode 100644
index 0000000000..82a6e34128
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwellCirclePiece.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Sprites;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Argon
+{
+ public class ArgonSwellCirclePiece : ArgonCirclePiece
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AccentColour = ColourInfo.GradientVertical(
+ new Color4(240, 201, 0, 255),
+ new Color4(167, 139, 0, 255)
+ );
+
+ AddInternal(new SpriteIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Icon = FontAwesome.Solid.Asterisk,
+ Size = new Vector2(ICON_SIZE),
+ Scale = new Vector2(0.8f, 1)
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonTickPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonTickPiece.cs
new file mode 100644
index 0000000000..df63d4948e
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonTickPiece.cs
@@ -0,0 +1,68 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
+using osuTK;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Argon
+{
+ public class ArgonTickPiece : CompositeDrawable
+ {
+ private readonly Bindable isFirstTick = new Bindable();
+
+ public ArgonTickPiece()
+ {
+ const float tick_size = 1 / TaikoHitObject.DEFAULT_SIZE * ArgonCirclePiece.ICON_SIZE;
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ RelativeSizeAxes = Axes.Both;
+ FillMode = FillMode.Fit;
+ Size = new Vector2(tick_size);
+ }
+
+ [Resolved]
+ private DrawableHitObject drawableHitObject { get; set; } = null!;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (drawableHitObject is DrawableDrumRollTick drumRollTick)
+ isFirstTick.BindTo(drumRollTick.IsFirstTick);
+
+ isFirstTick.BindValueChanged(first =>
+ {
+ if (first.NewValue)
+ {
+ InternalChild = new Circle
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both
+ };
+ }
+ else
+ {
+ InternalChild = new SpriteIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Icon = FontAwesome.Solid.AngleLeft,
+ Scale = new Vector2(0.8f, 1)
+ };
+ }
+ }, true);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/RingPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/RingPiece.cs
new file mode 100644
index 0000000000..534a1c71a3
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/RingPiece.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Argon
+{
+ public class RingPiece : CircularContainer
+ {
+ private readonly float relativeBorderThickness;
+
+ public RingPiece(float relativeBorderThickness)
+ {
+ this.relativeBorderThickness = relativeBorderThickness;
+ RelativeSizeAxes = Axes.Both;
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Masking = true;
+ BorderColour = Color4.White;
+
+ Child = new Box
+ {
+ AlwaysPresent = true,
+ Alpha = 0,
+ RelativeSizeAxes = Axes.Both
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ BorderThickness = relativeBorderThickness * DrawSize.Y;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs
new file mode 100644
index 0000000000..a5d091a1c8
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs
@@ -0,0 +1,74 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Argon
+{
+ public class TaikoArgonSkinTransformer : SkinTransformer
+ {
+ public TaikoArgonSkinTransformer(ISkin skin)
+ : base(skin)
+ {
+ }
+
+ public override Drawable? GetDrawableComponent(ISkinComponentLookup component)
+ {
+ switch (component)
+ {
+ case GameplaySkinComponentLookup resultComponent:
+ return new ArgonJudgementPiece(resultComponent.Component);
+
+ case TaikoSkinComponentLookup taikoComponent:
+ // TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries.
+ switch (taikoComponent.Component)
+ {
+ case TaikoSkinComponents.CentreHit:
+ return new ArgonCentreCirclePiece();
+
+ case TaikoSkinComponents.RimHit:
+ return new ArgonRimCirclePiece();
+
+ case TaikoSkinComponents.PlayfieldBackgroundLeft:
+ return new ArgonPlayfieldBackgroundLeft();
+
+ case TaikoSkinComponents.PlayfieldBackgroundRight:
+ return new ArgonPlayfieldBackgroundRight();
+
+ case TaikoSkinComponents.InputDrum:
+ return new ArgonInputDrum();
+
+ case TaikoSkinComponents.HitTarget:
+ return new ArgonHitTarget();
+
+ case TaikoSkinComponents.BarLine:
+ return new ArgonBarLine();
+
+ case TaikoSkinComponents.DrumRollBody:
+ return new ArgonElongatedCirclePiece();
+
+ case TaikoSkinComponents.DrumRollTick:
+ return new ArgonTickPiece();
+
+ case TaikoSkinComponents.TaikoExplosionKiai:
+ // the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield.
+ return Drawable.Empty().With(d => d.Expire());
+
+ case TaikoSkinComponents.TaikoExplosionGreat:
+ case TaikoSkinComponents.TaikoExplosionMiss:
+ case TaikoSkinComponents.TaikoExplosionOk:
+ return new ArgonHitExplosion(taikoComponent.Component);
+
+ case TaikoSkinComponents.Swell:
+ return new ArgonSwellCirclePiece();
+ }
+
+ break;
+ }
+
+ return base.GetDrawableComponent(component);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
index ccde8e6ac9..7ddc413d98 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
@@ -153,12 +153,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
updateStateTransforms(drawableHitObject, drawableHitObject.State.Value);
}
- private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
+ private void updateStateTransforms(DrawableHitObject h, ArmedState state)
{
+ if (h.HitObject is not Hit)
+ return;
+
switch (state)
{
case ArmedState.Hit:
- using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime))
+ using (BeginAbsoluteSequence(h.HitStateUpdateTime))
flashBox.FadeTo(0.9f).FadeOut(300);
break;
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultHitExplosion.cs
index b7ba76effa..2e76396a4d 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultHitExplosion.cs
@@ -10,6 +10,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
+using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning.Default
@@ -74,6 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
public void AnimateSecondHit()
{
+ this.ResizeTo(new Vector2(TaikoStrongableHitObject.STRONG_SCALE), 50);
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index dc36bc0320..fe12cf9765 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -24,6 +24,7 @@ using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Rulesets.Taiko.Scoring;
+using osu.Game.Rulesets.Taiko.Skinning.Argon;
using osu.Game.Rulesets.Taiko.Skinning.Legacy;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI;
@@ -47,6 +48,9 @@ namespace osu.Game.Rulesets.Taiko
{
switch (skin)
{
+ case ArgonSkin:
+ return new TaikoArgonSkinTransformer(skin);
+
case LegacySkin:
return new TaikoLegacySkinTransformer(skin);
}
diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
index d2b5811b56..8f2b702472 100644
--- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
@@ -90,7 +90,6 @@ namespace osu.Game.Rulesets.Taiko.UI
{
using (BeginAbsoluteSequence(secondHitTime.Value))
{
- this.ResizeTo(new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE), 50);
(skinnable.Drawable as IAnimatableHitExplosion)?.AnimateSecondHit();
}
}
diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs
index e7eb06c795..c9a78cbf59 100644
--- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs
+++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs
@@ -23,6 +23,7 @@ namespace osu.Game.Tests.Chat
private ChannelManager channelManager;
private int currentMessageId;
private List sentMessages;
+ private List silencedUserIds;
[SetUp]
public void Setup() => Schedule(() =>
@@ -39,6 +40,7 @@ namespace osu.Game.Tests.Chat
{
currentMessageId = 0;
sentMessages = new List();
+ silencedUserIds = new List();
((DummyAPIAccess)API).HandleRequest = req =>
{
@@ -55,6 +57,19 @@ namespace osu.Game.Tests.Chat
case MarkChannelAsReadRequest markRead:
handleMarkChannelAsReadRequest(markRead);
return true;
+
+ case ChatAckRequest ack:
+ ack.TriggerSuccess(new ChatAckResponse { Silences = silencedUserIds.Select(u => new ChatSilence { UserId = u }).ToList() });
+ silencedUserIds.Clear();
+ return true;
+
+ case GetUpdatesRequest updatesRequest:
+ updatesRequest.TriggerSuccess(new GetUpdatesResponse
+ {
+ Messages = sentMessages.ToList(),
+ Presence = new List()
+ });
+ return true;
}
return false;
@@ -95,6 +110,7 @@ namespace osu.Game.Tests.Chat
});
AddStep("post message", () => channelManager.PostMessage("Something interesting"));
+ AddUntilStep("message postesd", () => !channel.Messages.Any(m => m is LocalMessage));
AddStep("post /help command", () => channelManager.PostCommand("help", channel));
AddStep("post /me command with no action", () => channelManager.PostCommand("me", channel));
@@ -106,6 +122,28 @@ namespace osu.Game.Tests.Chat
AddAssert("channel's last read ID is set to the latest message", () => channel.LastReadId == sentMessages.Last().Id);
}
+ [Test]
+ public void TestSilencedUsersAreRemoved()
+ {
+ Channel channel = null;
+
+ AddStep("join channel and select it", () =>
+ {
+ channelManager.JoinChannel(channel = createChannel(1, ChannelType.Public));
+ channelManager.CurrentChannel.Value = channel;
+ });
+
+ AddStep("post message", () => channelManager.PostMessage("Definitely something bad"));
+
+ AddStep("mark user as silenced and send ack request", () =>
+ {
+ silencedUserIds.Add(API.LocalUser.Value.OnlineID);
+ channelManager.SendAck();
+ });
+
+ AddAssert("channel has no more messages", () => channel.Messages, () => Is.Empty);
+ }
+
private void handlePostMessageRequest(PostMessageRequest request)
{
var message = new Message(++currentMessageId)
@@ -115,7 +153,8 @@ namespace osu.Game.Tests.Chat
Content = request.Message.Content,
Links = request.Message.Links,
Timestamp = request.Message.Timestamp,
- Sender = request.Message.Sender
+ Sender = request.Message.Sender,
+ Uuid = request.Message.Uuid
};
sentMessages.Add(message);
diff --git a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs
index 1df3d336ee..e7fdb52d2f 100644
--- a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs
+++ b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs
@@ -65,7 +65,7 @@ namespace osu.Game.Tests.Database
private class TestLegacyBeatmapImporter : LegacyBeatmapImporter
{
public TestLegacyBeatmapImporter()
- : base(null)
+ : base(null!)
{
}
diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
index fb9d841d99..d5219f6391 100644
--- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Tests.Gameplay
// Apply a miss judgement
scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new TestJudgement()) { Type = HitResult.Miss });
- Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(0.0));
+ Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(0));
}
[Test]
diff --git a/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs b/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs
new file mode 100644
index 0000000000..81a3249efb
--- /dev/null
+++ b/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Graphics.Backgrounds;
+using osu.Framework.Graphics;
+using osuTK.Graphics;
+using osu.Framework.Graphics.Shapes;
+
+namespace osu.Game.Tests.Visual.Background
+{
+ public class TestSceneTrianglesBackground : OsuTestScene
+ {
+ private readonly Triangles triangles;
+
+ public TestSceneTrianglesBackground()
+ {
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black
+ },
+ triangles = new Triangles
+ {
+ RelativeSizeAxes = Axes.Both,
+ ColourLight = Color4.White,
+ ColourDark = Color4.Gray
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ AddSliderStep("Triangle scale", 0f, 10f, 1f, s => triangles.TriangleScale = s);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
index 981967e413..ee6c322ee3 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
@@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.Editing
}
[Test]
- public void TestSharedClockState()
+ public void TestClockTimeTransferIsOneDirectional()
{
AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000));
AddStep("click test gameplay button", () =>
@@ -195,15 +195,15 @@ namespace osu.Game.Tests.Visual.Editing
GameplayClockContainer gameplayClockContainer = null;
AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType().First());
AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning);
+ // when the gameplay test is entered, the clock is expected to continue from where it was in the main editor...
AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000);
- double timeAtPlayerExit = 0;
AddWaitStep("wait some", 5);
- AddStep("store time before exit", () => timeAtPlayerExit = gameplayClockContainer.CurrentTime);
AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
- AddAssert("time is past player exit", () => EditorClock.CurrentTime >= timeAtPlayerExit);
+ // but when exiting from gameplay test back to editor, the expectation is that the editor time should revert to what it was at the point of initiating the gameplay test.
+ AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000));
}
public override void TearDownSteps()
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs
index 171ae829a9..30d2cc6423 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
private TestGameplayLeaderboard leaderboard;
- private readonly BindableDouble playerScore = new BindableDouble();
+ private readonly BindableLong playerScore = new BindableLong();
public TestSceneGameplayLeaderboard()
{
@@ -76,8 +76,8 @@ namespace osu.Game.Tests.Visual.Gameplay
createLeaderboard();
addLocalPlayer();
- var player2Score = new BindableDouble(1234567);
- var player3Score = new BindableDouble(1111111);
+ var player2Score = new BindableLong(1234567);
+ var player3Score = new BindableLong(1111111);
AddStep("add player 2", () => createLeaderboardScore(player2Score, new APIUser { Username = "Player 2" }));
AddStep("add player 3", () => createLeaderboardScore(player3Score, new APIUser { Username = "Player 3" }));
@@ -161,9 +161,9 @@ namespace osu.Game.Tests.Visual.Gameplay
});
}
- private void createRandomScore(APIUser user) => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), user);
+ private void createRandomScore(APIUser user) => createLeaderboardScore(new BindableLong(RNG.Next(0, 5_000_000)), user);
- private void createLeaderboardScore(BindableDouble score, APIUser user, bool isTracked = false)
+ private void createLeaderboardScore(BindableLong score, APIUser user, bool isTracked = false)
{
var leaderboardScore = leaderboard.Add(user, isTracked);
leaderboardScore.TotalScore.BindTo(score);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
index 7c668adba5..b90b9b437d 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
@@ -163,10 +163,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for bars to disappear", () => !this.ChildrenOfType().Any());
AddUntilStep("ensure max circles not exceeded", () =>
- {
- return this.ChildrenOfType()
- .All(m => m.ChildrenOfType().Count() <= max_displayed_judgements);
- });
+ this.ChildrenOfType().First().ChildrenOfType().Count(), () => Is.LessThanOrEqualTo(max_displayed_judgements));
AddStep("show displays", () =>
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs
index 60ed0012ae..881870921c 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs
@@ -15,6 +15,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
+using osu.Game.Screens.Select;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -26,6 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private readonly BindableList scores = new BindableList();
private readonly Bindable configVisibility = new Bindable();
+ private readonly Bindable beatmapTabType = new Bindable();
private SoloGameplayLeaderboard leaderboard = null!;
@@ -33,6 +35,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility);
+ config.BindWith(OsuSetting.BeatmapDetailTab, beatmapTabType);
}
[SetUpSteps]
@@ -70,6 +73,25 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value);
}
+ [TestCase(PlayBeatmapDetailArea.TabType.Local, 51)]
+ [TestCase(PlayBeatmapDetailArea.TabType.Global, null)]
+ [TestCase(PlayBeatmapDetailArea.TabType.Country, null)]
+ [TestCase(PlayBeatmapDetailArea.TabType.Friends, null)]
+ public void TestTrackedScorePosition(PlayBeatmapDetailArea.TabType tabType, int? expectedOverflowIndex)
+ {
+ AddStep($"change TabType to {tabType}", () => beatmapTabType.Value = tabType);
+ AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50));
+
+ AddStep("add one more score", () => scores.Add(new ScoreInfo { User = new APIUser { Username = "New player 1" }, TotalScore = RNG.Next(600000, 1000000) }));
+
+ AddUntilStep("wait for sort", () => leaderboard.ChildrenOfType().First().ScorePosition != null);
+
+ if (expectedOverflowIndex == null)
+ AddUntilStep("tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null);
+ else
+ AddUntilStep($"tracked player is #{expectedOverflowIndex}", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(expectedOverflowIndex));
+ }
+
[Test]
public void TestVisibility()
{
@@ -95,7 +117,7 @@ namespace osu.Game.Tests.Visual.Gameplay
new ScoreInfo { User = new APIUser { Username = @"spaceman_atlas" }, TotalScore = RNG.Next(500000, 1000000) },
new ScoreInfo { User = new APIUser { Username = @"frenzibyte" }, TotalScore = RNG.Next(500000, 1000000) },
new ScoreInfo { User = new APIUser { Username = @"Susko3" }, TotalScore = RNG.Next(500000, 1000000) },
- }.Concat(Enumerable.Range(0, 50).Select(i => new ScoreInfo { User = new APIUser { Username = $"User {i + 1}" }, TotalScore = 1000000 - i * 10000 })).ToList();
+ }.Concat(Enumerable.Range(0, 44).Select(i => new ScoreInfo { User = new APIUser { Username = $"User {i + 1}" }, TotalScore = 1000000 - i * 10000 })).ToList();
}
}
}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs
index 5bd879de14..c3d7bde68f 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs
@@ -56,6 +56,7 @@ namespace osu.Game.Tests.Visual.Navigation
PushAndConfirm(() => new PlaySongSelect());
AddUntilStep("wait for selection", () => !Game.Beatmap.IsDefault);
+ AddUntilStep("wait for carousel load", () => songSelect.BeatmapSetsLoaded);
AddStep("enter gameplay", () => InputManager.Key(Key.Enter));
@@ -92,6 +93,8 @@ namespace osu.Game.Tests.Visual.Navigation
.AsEnumerable()
.First(k => k.RulesetName == "osu" && k.ActionInt == 0);
+ private Screens.Select.SongSelect songSelect => Game.ScreenStack.CurrentScreen as Screens.Select.SongSelect;
+
private Player player => Game.ScreenStack.CurrentScreen as Player;
private KeyCounter keyCounter => player.ChildrenOfType().First();
diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
index d1b1ce5c4b..ce0543875b 100644
--- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
@@ -4,6 +4,7 @@
#nullable disable
using System.Linq;
+using System.Threading;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
@@ -85,6 +86,19 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("did perform", () => actionPerformed);
}
+ [Test]
+ public void TestPerformEnsuresScreenIsLoaded()
+ {
+ TestLoadBlockingScreen screen = null;
+
+ AddStep("push blocking screen", () => Game.ScreenStack.Push(screen = new TestLoadBlockingScreen()));
+ AddStep("perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(TestLoadBlockingScreen) }));
+ AddAssert("action not performed", () => !actionPerformed);
+
+ AddStep("allow load", () => screen.LoadEvent.Set());
+ AddUntilStep("action performed", () => actionPerformed);
+ }
+
[Test]
public void TestOverlaysAlwaysClosed()
{
@@ -270,5 +284,16 @@ namespace osu.Game.Tests.Visual.Navigation
return base.OnExiting(e);
}
}
+
+ public class TestLoadBlockingScreen : OsuScreen
+ {
+ public readonly ManualResetEventSlim LoadEvent = new ManualResetEventSlim();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ LoadEvent.Wait(10000);
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs
index 6469962b08..02b348b439 100644
--- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs
@@ -7,12 +7,14 @@ using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
+using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Menu;
+using osu.Game.Screens.Select;
namespace osu.Game.Tests.Visual.Navigation
{
@@ -55,6 +57,7 @@ namespace osu.Game.Tests.Visual.Navigation
presentAndConfirm(firstImport);
var secondImport = importBeatmap(3);
+ confirmBeatmapInSongSelect(secondImport);
presentAndConfirm(secondImport);
// Test presenting same beatmap more than once
@@ -74,6 +77,7 @@ namespace osu.Game.Tests.Visual.Navigation
presentAndConfirm(firstImport);
var secondImport = importBeatmap(3, new ManiaRuleset().RulesetInfo);
+ confirmBeatmapInSongSelect(secondImport);
presentAndConfirm(secondImport);
presentSecondDifficultyAndConfirm(firstImport, 1);
@@ -134,13 +138,22 @@ namespace osu.Game.Tests.Visual.Navigation
return () => imported;
}
+ private void confirmBeatmapInSongSelect(Func getImport)
+ {
+ AddUntilStep("beatmap in song select", () =>
+ {
+ var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen;
+ return songSelect.ChildrenOfType().Single().BeatmapSets.Any(b => b.MatchesOnlineID(getImport()));
+ });
+ }
+
private void presentAndConfirm(Func getImport)
{
AddStep("present beatmap", () => Game.PresentBeatmap(getImport()));
- AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect);
- AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapSetInfo.MatchesOnlineID(getImport()));
- AddAssert("correct ruleset selected", () => Game.Ruleset.Value.Equals(getImport().Beatmaps.First().Ruleset));
+ AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.IsLoaded);
+ AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineID, () => Is.EqualTo(getImport().OnlineID));
+ AddAssert("correct ruleset selected", () => Game.Ruleset.Value, () => Is.EqualTo(getImport().Beatmaps.First().Ruleset));
}
private void presentSecondDifficultyAndConfirm(Func getImport, int importedID)
@@ -148,9 +161,9 @@ namespace osu.Game.Tests.Visual.Navigation
Predicate pred = b => b.OnlineID == importedID * 2048;
AddStep("present difficulty", () => Game.PresentBeatmap(getImport(), pred));
- AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect);
- AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID == importedID * 2048);
- AddAssert("correct ruleset selected", () => Game.Ruleset.Value.Equals(getImport().Beatmaps.First().Ruleset));
+ AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.IsLoaded);
+ AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(importedID * 2048));
+ AddAssert("correct ruleset selected", () => Game.Ruleset.Value, () => Is.EqualTo(getImport().Beatmaps.First().Ruleset));
}
}
}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index d58887c090..e500efede3 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -436,6 +436,8 @@ namespace osu.Game.Tests.Visual.Navigation
{
AddUntilStep("Wait for toolbar to load", () => Game.Toolbar.IsLoaded);
+ AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
+
TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
index a537f0660c..de44986001 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
@@ -46,6 +46,8 @@ namespace osu.Game.Tests.Visual.Online
availableChannels.Add(new Channel { Name = "#english" });
availableChannels.Add(new Channel { Name = "#japanese" });
Dependencies.Cache(chatManager);
+
+ Add(chatManager);
}
[SetUp]
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
index 0b982a5745..0b75a2aa05 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
@@ -40,8 +40,10 @@ namespace osu.Game.Tests.Visual.Online
private ChannelManager channelManager;
private readonly APIUser testUser = new APIUser { Username = "test user", Id = 5071479 };
+ private readonly APIUser testUser1 = new APIUser { Username = "test user", Id = 5071480 };
private Channel[] testChannels;
+ private Message[] initialMessages;
private Channel testChannel1 => testChannels[0];
private Channel testChannel2 => testChannels[1];
@@ -49,10 +51,14 @@ namespace osu.Game.Tests.Visual.Online
[Resolved]
private OsuConfigManager config { get; set; } = null!;
+ private int currentMessageId;
+
[SetUp]
public void SetUp() => Schedule(() =>
{
+ currentMessageId = 0;
testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray();
+ initialMessages = testChannels.SelectMany(createChannelMessages).ToArray();
Child = new DependencyProvidingContainer
{
@@ -99,7 +105,7 @@ namespace osu.Game.Tests.Visual.Online
return true;
case GetMessagesRequest getMessages:
- getMessages.TriggerSuccess(createChannelMessages(getMessages.Channel));
+ getMessages.TriggerSuccess(initialMessages.ToList());
return true;
case GetUserRequest getUser:
@@ -495,6 +501,35 @@ namespace osu.Game.Tests.Visual.Online
waitForChannel1Visible();
}
+ [Test]
+ public void TestRemoveMessages()
+ {
+ AddStep("Show overlay with channel", () =>
+ {
+ chatOverlay.Show();
+ channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel1);
+ });
+
+ AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
+ waitForChannel1Visible();
+
+ AddStep("Send message from another user", () =>
+ {
+ testChannel1.AddNewMessages(new Message
+ {
+ ChannelId = testChannel1.Id,
+ Content = "Message from another user",
+ Timestamp = DateTimeOffset.Now,
+ Sender = testUser1,
+ });
+ });
+
+ AddStep("Remove messages from other user", () =>
+ {
+ testChannel1.RemoveMessagesFromUser(testUser.Id);
+ });
+ }
+
private void joinTestChannel(int i)
{
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));
@@ -546,7 +581,7 @@ namespace osu.Game.Tests.Visual.Online
private List createChannelMessages(Channel channel)
{
- var message = new Message
+ var message = new Message(currentMessageId++)
{
ChannelId = channel.Id,
Content = $"Hello, this is a message in {channel.Name}",
diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
index 292facab11..34ecad7dc1 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
@@ -56,7 +56,9 @@ namespace osu.Game.Tests.Visual.Online
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
- Add(channelManager = new ChannelManager(parent.Get()));
+ var api = parent.Get();
+
+ Add(channelManager = new ChannelManager(api));
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
index d7d073e908..bd54591b9b 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
@@ -40,6 +40,43 @@ namespace osu.Game.Tests.Visual.Settings
AddWaitStep("wait for scroll", 5);
}
+ [Test]
+ public void TestBindingTwoNonModifiers()
+ {
+ AddStep("press j", () => InputManager.PressKey(Key.J));
+ scrollToAndStartBinding("Increase volume");
+ AddStep("press k", () => InputManager.Key(Key.K));
+ AddStep("release j", () => InputManager.ReleaseKey(Key.J));
+ checkBinding("Increase volume", "K");
+ }
+
+ [Test]
+ public void TestBindingSingleKey()
+ {
+ scrollToAndStartBinding("Increase volume");
+ AddStep("press k", () => InputManager.Key(Key.K));
+ checkBinding("Increase volume", "K");
+ }
+
+ [Test]
+ public void TestBindingSingleModifier()
+ {
+ scrollToAndStartBinding("Increase volume");
+ AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft));
+ AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
+ checkBinding("Increase volume", "LShift");
+ }
+
+ [Test]
+ public void TestBindingSingleKeyWithModifier()
+ {
+ scrollToAndStartBinding("Increase volume");
+ AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft));
+ AddStep("press k", () => InputManager.Key(Key.K));
+ AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
+ checkBinding("Increase volume", "LShift-K");
+ }
+
[Test]
public void TestBindingMouseWheelToNonGameplay()
{
@@ -169,7 +206,8 @@ namespace osu.Game.Tests.Visual.Settings
AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0);
- AddAssert("binding cleared", () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0)));
+ AddAssert("binding cleared",
+ () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0)));
}
[Test]
@@ -198,7 +236,8 @@ namespace osu.Game.Tests.Visual.Settings
AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0);
- AddAssert("binding cleared", () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0)));
+ AddAssert("binding cleared",
+ () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0)));
}
[Test]
@@ -256,8 +295,8 @@ namespace osu.Game.Tests.Visual.Settings
var firstRow = panel.ChildrenOfType().First(r => r.ChildrenOfType().Any(s => s.Text.ToString() == name));
var firstButton = firstRow.ChildrenOfType().First();
- return firstButton.Text.Text == keyName;
- });
+ return firstButton.Text.Text.ToString();
+ }, () => Is.EqualTo(keyName));
}
private void scrollToAndStartBinding(string name)
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 63532fdba8..b6b9e8926b 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -1055,6 +1055,18 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("mod overlay hidden", () => songSelect!.ModSelect.State.Value == Visibility.Hidden);
}
+ [Test]
+ public void TestBeatmapOptionsDisabled()
+ {
+ createSongSelect();
+
+ addRulesetImportStep(0);
+
+ AddAssert("options enabled", () => songSelect.ChildrenOfType().Single().Enabled.Value);
+ AddStep("delete all beatmaps", () => manager.Delete());
+ AddAssert("options disabled", () => !songSelect.ChildrenOfType().Single().Enabled.Value);
+ }
+
private void waitForInitialSelection()
{
AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault);
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs
index cb78fbfe35..0a88abface 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs
@@ -3,8 +3,10 @@
#nullable disable
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Framework.Testing;
using osu.Game.Screens.Select;
using osuTK;
using osuTK.Input;
@@ -43,6 +45,12 @@ namespace osu.Game.Tests.Visual.SongSelect
InputManager.MoveMouseTo(Vector2.Zero);
});
+ [Test]
+ public void TestState()
+ {
+ AddRepeatStep("toggle options state", () => this.ChildrenOfType().Last().Enabled.Toggle(), 20);
+ }
+
[Test]
public void TestFooterRandom()
{
diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs
index 6a8e4aa951..ca2b400e8b 100644
--- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs
+++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Tournament.Components
if (manager == null)
{
- AddInternal(manager = new ChannelManager(api) { HighPollRate = { Value = true } });
+ AddInternal(manager = new ChannelManager(api));
Channel.BindTo(manager.CurrentChannel);
}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/ExpandedContentScrollContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/ExpandedContentScrollContainer.cs
index a80a7998a5..c0ed6ac1a9 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/ExpandedContentScrollContainer.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/ExpandedContentScrollContainer.cs
@@ -59,6 +59,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
return base.OnScroll(e);
}
+ protected override bool OnClick(ClickEvent e) => true;
+
private class ExpandedContentScrollbar : OsuScrollbar
{
public ExpandedContentScrollbar(Direction scrollDir)
diff --git a/osu.Game/Configuration/IGameplaySettings.cs b/osu.Game/Configuration/IGameplaySettings.cs
index a35bdd20d0..8d66535017 100644
--- a/osu.Game/Configuration/IGameplaySettings.cs
+++ b/osu.Game/Configuration/IGameplaySettings.cs
@@ -8,7 +8,7 @@ namespace osu.Game.Configuration
{
///
/// A settings provider which generally sources from (global user settings)
- /// but can allow overriding settings by caching more locally. For instance, in the editor.
+ /// but can allow overriding settings by caching more locally. For instance, in the editor compose screen.
///
///
/// More settings can be moved into this interface as required.
diff --git a/osu.Game/Database/LegacyBeatmapImporter.cs b/osu.Game/Database/LegacyBeatmapImporter.cs
index 0955461609..20add54949 100644
--- a/osu.Game/Database/LegacyBeatmapImporter.cs
+++ b/osu.Game/Database/LegacyBeatmapImporter.cs
@@ -1,11 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.IO.Stores;
+using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.IO;
@@ -22,22 +22,42 @@ namespace osu.Game.Database
{
// make sure the directory exists
if (!storage.ExistsDirectory(string.Empty))
- yield break;
+ return Array.Empty();
- foreach (string directory in storage.GetDirectories(string.Empty))
+ List paths = new List();
+
+ try
{
- var directoryStorage = storage.GetStorageForDirectory(directory);
-
- if (!directoryStorage.GetFiles(string.Empty).ExcludeSystemFileNames().Any())
+ foreach (string directory in storage.GetDirectories(string.Empty))
{
- // if a directory doesn't contain files, attempt looking for beatmaps inside of that directory.
- // this is a special behaviour in stable for beatmaps only, see https://github.com/ppy/osu/issues/18615.
- foreach (string subDirectory in GetStableImportPaths(directoryStorage))
- yield return subDirectory;
+ var directoryStorage = storage.GetStorageForDirectory(directory);
+
+ try
+ {
+ if (!directoryStorage.GetFiles(string.Empty).ExcludeSystemFileNames().Any())
+ {
+ // if a directory doesn't contain files, attempt looking for beatmaps inside of that directory.
+ // this is a special behaviour in stable for beatmaps only, see https://github.com/ppy/osu/issues/18615.
+ foreach (string subDirectory in GetStableImportPaths(directoryStorage))
+ paths.Add(subDirectory);
+ }
+ else
+ paths.Add(storage.GetFullPath(directory));
+ }
+ catch (Exception e)
+ {
+ // Catch any errors when enumerating files
+ Logger.Log($"Error when enumerating files in {directoryStorage.GetFullPath(string.Empty)}: {e}");
+ }
}
- else
- yield return storage.GetFullPath(directory);
}
+ catch (Exception e)
+ {
+ // Catch any errors when enumerating directories
+ Logger.Log($"Error when enumerating directories in {storage.GetFullPath(string.Empty)}: {e}");
+ }
+
+ return paths;
}
public LegacyBeatmapImporter(IModelImporter importer)
diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs
index 1166a86814..09d137011c 100644
--- a/osu.Game/Graphics/Backgrounds/Triangles.cs
+++ b/osu.Game/Graphics/Backgrounds/Triangles.cs
@@ -17,6 +17,7 @@ using System.Collections.Generic;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Lists;
+using osu.Framework.Bindables;
namespace osu.Game.Graphics.Backgrounds
{
@@ -25,6 +26,11 @@ namespace osu.Game.Graphics.Backgrounds
private const float triangle_size = 100;
private const float base_velocity = 50;
+ ///
+ /// sqrt(3) / 2
+ ///
+ private const float equilateral_triangle_ratio = 0.866f;
+
///
/// How many screen-space pixels are smoothed over.
/// Same behavior as Sprite's EdgeSmoothness.
@@ -69,7 +75,13 @@ namespace osu.Game.Graphics.Backgrounds
///
protected virtual float SpawnRatio => 1;
- private float triangleScale = 1;
+ private readonly BindableFloat triangleScale = new BindableFloat(1f);
+
+ public float TriangleScale
+ {
+ get => triangleScale.Value;
+ set => triangleScale.Value = value;
+ }
///
/// Whether we should drop-off alpha values of triangles more quickly to improve
@@ -103,30 +115,13 @@ namespace osu.Game.Graphics.Backgrounds
private void load(IRenderer renderer, ShaderManager shaders)
{
texture = renderer.WhitePixel;
- shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
+ shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
}
protected override void LoadComplete()
{
base.LoadComplete();
- addTriangles(true);
- }
-
- public float TriangleScale
- {
- get => triangleScale;
- set
- {
- float change = value / triangleScale;
- triangleScale = value;
-
- for (int i = 0; i < parts.Count; i++)
- {
- TriangleParticle newParticle = parts[i];
- newParticle.Scale *= change;
- parts[i] = newParticle;
- }
- }
+ triangleScale.BindValueChanged(_ => Reset(), true);
}
protected override void Update()
@@ -147,7 +142,7 @@ namespace osu.Game.Graphics.Backgrounds
// Since position is relative, the velocity needs to scale inversely with DrawHeight.
// Since we will later multiply by the scale of individual triangles we normalize by
// dividing by triangleScale.
- float movedDistance = -elapsedSeconds * Velocity * base_velocity / (DrawHeight * triangleScale);
+ float movedDistance = -elapsedSeconds * Velocity * base_velocity / (DrawHeight * TriangleScale);
for (int i = 0; i < parts.Count; i++)
{
@@ -159,7 +154,7 @@ namespace osu.Game.Graphics.Backgrounds
parts[i] = newParticle;
- float bottomPos = parts[i].Position.Y + triangle_size * parts[i].Scale * 0.866f / DrawHeight;
+ float bottomPos = parts[i].Position.Y + triangle_size * parts[i].Scale * equilateral_triangle_ratio / DrawHeight;
if (bottomPos < 0)
parts.RemoveAt(i);
}
@@ -185,9 +180,11 @@ namespace osu.Game.Graphics.Backgrounds
// Limited by the maximum size of QuadVertexBuffer for safety.
const int max_triangles = ushort.MaxValue / (IRenderer.VERTICES_PER_QUAD + 2);
- AimCount = (int)Math.Min(max_triangles, (DrawWidth * DrawHeight * 0.002f / (triangleScale * triangleScale) * SpawnRatio));
+ AimCount = (int)Math.Min(max_triangles, DrawWidth * DrawHeight * 0.002f / (TriangleScale * TriangleScale) * SpawnRatio);
- for (int i = 0; i < AimCount - parts.Count; i++)
+ int currentCount = parts.Count;
+
+ for (int i = 0; i < AimCount - currentCount; i++)
parts.Add(createTriangle(randomY));
}
@@ -195,13 +192,27 @@ namespace osu.Game.Graphics.Backgrounds
{
TriangleParticle particle = CreateTriangle();
- particle.Position = new Vector2(nextRandom(), randomY ? nextRandom() : 1);
+ particle.Position = getRandomPosition(randomY, particle.Scale);
particle.ColourShade = nextRandom();
particle.Colour = CreateTriangleShade(particle.ColourShade);
return particle;
}
+ private Vector2 getRandomPosition(bool randomY, float scale)
+ {
+ float y = 1;
+
+ if (randomY)
+ {
+ // since triangles are drawn from the top - allow them to be positioned a bit above the screen
+ float maxOffset = triangle_size * scale * equilateral_triangle_ratio / DrawHeight;
+ y = Interpolation.ValueAt(nextRandom(), -maxOffset, 1f, 0f, 1f);
+ }
+
+ return new Vector2(nextRandom(), y);
+ }
+
///
/// Creates a triangle particle with a random scale.
///
@@ -214,7 +225,7 @@ namespace osu.Game.Graphics.Backgrounds
float u1 = 1 - nextRandom(); //uniform(0,1] random floats
float u2 = 1 - nextRandom();
float randStdNormal = (float)(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2)); // random normal(0,1)
- float scale = Math.Max(triangleScale * (mean + std_dev * randStdNormal), 0.1f); // random normal(mean,stdDev^2)
+ float scale = Math.Max(TriangleScale * (mean + std_dev * randStdNormal), 0.1f); // random normal(mean,stdDev^2)
return new TriangleParticle { Scale = scale };
}
@@ -284,7 +295,7 @@ namespace osu.Game.Graphics.Backgrounds
foreach (TriangleParticle particle in parts)
{
- var offset = triangle_size * new Vector2(particle.Scale * 0.5f, particle.Scale * 0.866f);
+ var offset = triangle_size * new Vector2(particle.Scale * 0.5f, particle.Scale * equilateral_triangle_ratio);
var triangle = new Triangle(
Vector2Extensions.Transform(particle.Position * size, DrawInfo.Matrix),
diff --git a/osu.Game/Graphics/Sprites/LogoAnimation.cs b/osu.Game/Graphics/Sprites/LogoAnimation.cs
index 7debdc7a37..097de4dfcb 100644
--- a/osu.Game/Graphics/Sprites/LogoAnimation.cs
+++ b/osu.Game/Graphics/Sprites/LogoAnimation.cs
@@ -17,7 +17,6 @@ namespace osu.Game.Graphics.Sprites
private void load(ShaderManager shaders)
{
TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation");
- RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation"); // Masking isn't supported for now
}
private float animationProgress;
@@ -58,7 +57,7 @@ namespace osu.Game.Graphics.Sprites
protected override void Blit(IRenderer renderer)
{
- GetAppropriateShader(renderer).GetUniform("progress").UpdateValue(ref progress);
+ TextureShader.GetUniform("progress").UpdateValue(ref progress);
base.Blit(renderer);
}
diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs
index 9140815f32..dae5de2d65 100644
--- a/osu.Game/Graphics/UserInterface/OsuButton.cs
+++ b/osu.Game/Graphics/UserInterface/OsuButton.cs
@@ -4,6 +4,7 @@
#nullable disable
using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -69,6 +70,8 @@ namespace osu.Game.Graphics.UserInterface
protected Box Background;
protected SpriteText SpriteText;
+ private readonly Box flashLayer;
+
public OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Button)
{
Height = 40;
@@ -99,6 +102,14 @@ namespace osu.Game.Graphics.UserInterface
Depth = float.MinValue
},
SpriteText = CreateText(),
+ flashLayer = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
+ Depth = float.MinValue,
+ Colour = Color4.White.Opacity(0.5f),
+ Alpha = 0,
+ },
}
});
@@ -125,7 +136,7 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnClick(ClickEvent e)
{
if (Enabled.Value)
- Background.FlashColour(Color4.White, 800, Easing.OutQuint);
+ flashLayer.FadeOutFromOne(800, Easing.OutQuint);
return base.OnClick(e);
}
diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs
index e46e2b31ac..2efe27c842 100644
--- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs
+++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs
@@ -11,7 +11,7 @@ using osu.Game.Graphics.Sprites;
namespace osu.Game.Graphics.UserInterface
{
- public abstract class ScoreCounter : RollingCounter
+ public abstract class ScoreCounter : RollingCounter
{
protected override double RollingDuration => 1000;
protected override Easing RollingEasing => Easing.Out;
@@ -36,10 +36,10 @@ namespace osu.Game.Graphics.UserInterface
UpdateDisplay();
}
- protected override double GetProportionalDuration(double currentValue, double newValue) =>
+ protected override double GetProportionalDuration(long currentValue, long newValue) =>
currentValue > newValue ? currentValue - newValue : newValue - currentValue;
- protected override LocalisableString FormatCount(double count) => ((long)count).ToLocalisableString(formatString);
+ protected override LocalisableString FormatCount(long count) => count.ToLocalisableString(formatString);
protected override OsuSpriteText CreateSpriteText()
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true));
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index a0c8e0d555..65f78e1fd7 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -20,6 +20,8 @@ using osu.Framework.Logging;
using osu.Game.Configuration;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Notifications;
+using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Users;
namespace osu.Game.Online.API
@@ -299,6 +301,9 @@ namespace osu.Game.Online.API
public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) =>
new HubClientConnector(clientName, endpoint, this, versionHash, preferMessagePack);
+ public NotificationsClientConnector GetNotificationsConnector() =>
+ new WebSocketNotificationsClientConnector(this);
+
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
{
Debug.Assert(State.Value == APIState.Offline);
@@ -414,7 +419,7 @@ namespace osu.Game.Online.API
failureCount++;
log.Add($@"API failure count is now {failureCount}");
- if (failureCount >= 3 && State.Value == APIState.Online)
+ if (failureCount >= 3)
{
state.Value = APIState.Failing;
flushQueue();
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index 7dc34d1293..609efd8ab6 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.cs
@@ -9,6 +9,8 @@ using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Notifications;
+using osu.Game.Tests;
using osu.Game.Users;
namespace osu.Game.Online.API
@@ -115,6 +117,8 @@ namespace osu.Game.Online.API
public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null;
+ public NotificationsClientConnector GetNotificationsConnector() => new PollingNotificationsClientConnector(this);
+
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
{
Thread.Sleep(200);
diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs
index a90b11e354..6054effaa1 100644
--- a/osu.Game/Online/API/IAPIProvider.cs
+++ b/osu.Game/Online/API/IAPIProvider.cs
@@ -5,6 +5,7 @@ using System;
using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Notifications;
using osu.Game.Users;
namespace osu.Game.Online.API
@@ -112,6 +113,11 @@ namespace osu.Game.Online.API
/// Whether to use MessagePack for serialisation if available on this platform.
IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack = true);
+ ///
+ /// Constructs a new .
+ ///
+ NotificationsClientConnector GetNotificationsConnector();
+
///
/// Create a new user account. This is a blocking operation.
///
diff --git a/osu.Game/Online/API/Requests/ChatAckRequest.cs b/osu.Game/Online/API/Requests/ChatAckRequest.cs
new file mode 100644
index 0000000000..306b5acc1d
--- /dev/null
+++ b/osu.Game/Online/API/Requests/ChatAckRequest.cs
@@ -0,0 +1,39 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Net.Http;
+using osu.Framework.IO.Network;
+using osu.Game.Online.API.Requests.Responses;
+
+namespace osu.Game.Online.API.Requests
+{
+ ///
+ /// A request which should be sent occasionally while interested in chat and online state.
+ ///
+ /// This will:
+ /// - Mark the user as "online" (for 10 minutes since the last invocation).
+ /// - Return any silences since the last invocation (if either or is not null).
+ ///
+ /// For silence handling, a should be provided as soon as a message is received by the client.
+ /// From that point forward, should be preferred after the first
+ /// arrives in a response from the ack request. Specifying both parameters will prioritise the latter.
+ ///
+ public class ChatAckRequest : APIRequest
+ {
+ public long? SinceMessageId;
+ public uint? SinceSilenceId;
+
+ protected override WebRequest CreateWebRequest()
+ {
+ var req = base.CreateWebRequest();
+ req.Method = HttpMethod.Post;
+ if (SinceMessageId != null)
+ req.AddParameter(@"since", SinceMessageId.ToString());
+ if (SinceSilenceId != null)
+ req.AddParameter(@"history_since", SinceSilenceId.Value.ToString());
+ return req;
+ }
+
+ protected override string Target => "chat/ack";
+ }
+}
diff --git a/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs b/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs
index dea94bfce2..6b7192dbf4 100644
--- a/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs
+++ b/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs
@@ -28,6 +28,7 @@ namespace osu.Game.Online.API.Requests
req.AddParameter(@"target_id", user.Id.ToString());
req.AddParameter(@"message", message.Content);
req.AddParameter(@"is_action", message.IsAction.ToString().ToLowerInvariant());
+ req.AddParameter(@"uuid", message.Uuid);
return req;
}
diff --git a/osu.Game/Online/API/Requests/GetChannelRequest.cs b/osu.Game/Online/API/Requests/GetChannelRequest.cs
new file mode 100644
index 0000000000..5bc9cb519a
--- /dev/null
+++ b/osu.Game/Online/API/Requests/GetChannelRequest.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Online.API.Requests.Responses;
+
+namespace osu.Game.Online.API.Requests
+{
+ public class GetChannelRequest : APIRequest
+ {
+ private readonly long channelId;
+
+ public GetChannelRequest(long channelId)
+ {
+ this.channelId = channelId;
+ }
+
+ protected override string Target => $"chat/channels/{channelId}";
+ }
+}
diff --git a/osu.Game/Online/API/Requests/GetNotificationsRequest.cs b/osu.Game/Online/API/Requests/GetNotificationsRequest.cs
new file mode 100644
index 0000000000..afd4da296e
--- /dev/null
+++ b/osu.Game/Online/API/Requests/GetNotificationsRequest.cs
@@ -0,0 +1,12 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Online.API.Requests.Responses;
+
+namespace osu.Game.Online.API.Requests
+{
+ public class GetNotificationsRequest : APIRequest
+ {
+ protected override string Target => @"notifications";
+ }
+}
diff --git a/osu.Game/Online/API/Requests/GetScoresRequest.cs b/osu.Game/Online/API/Requests/GetScoresRequest.cs
index 966e69938c..7d1d26b75d 100644
--- a/osu.Game/Online/API/Requests/GetScoresRequest.cs
+++ b/osu.Game/Online/API/Requests/GetScoresRequest.cs
@@ -16,6 +16,8 @@ namespace osu.Game.Online.API.Requests
{
public class GetScoresRequest : APIRequest
{
+ public const int MAX_SCORES_PER_REQUEST = 50;
+
private readonly IBeatmapInfo beatmapInfo;
private readonly BeatmapLeaderboardScope scope;
private readonly IRulesetInfo ruleset;
diff --git a/osu.Game/Online/API/Requests/PostMessageRequest.cs b/osu.Game/Online/API/Requests/PostMessageRequest.cs
index 7b20bd9ad5..e3709d8f13 100644
--- a/osu.Game/Online/API/Requests/PostMessageRequest.cs
+++ b/osu.Game/Online/API/Requests/PostMessageRequest.cs
@@ -25,6 +25,7 @@ namespace osu.Game.Online.API.Requests
req.Method = HttpMethod.Post;
req.AddParameter(@"is_action", Message.IsAction.ToString().ToLowerInvariant());
req.AddParameter(@"message", Message.Content);
+ req.AddParameter(@"uuid", Message.Uuid);
return req;
}
diff --git a/osu.Game/Online/API/Requests/Responses/APINotification.cs b/osu.Game/Online/API/Requests/Responses/APINotification.cs
new file mode 100644
index 0000000000..de856c0333
--- /dev/null
+++ b/osu.Game/Online/API/Requests/Responses/APINotification.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace osu.Game.Online.API.Requests.Responses
+{
+ [JsonObject(MemberSerialization.OptIn)]
+ public class APINotification
+ {
+ [JsonProperty(@"id")]
+ public long Id { get; set; }
+
+ [JsonProperty(@"name")]
+ public string Name { get; set; } = null!;
+
+ [JsonProperty(@"created_at")]
+ public DateTimeOffset? CreatedAt { get; set; }
+
+ [JsonProperty(@"object_type")]
+ public string ObjectType { get; set; } = null!;
+
+ [JsonProperty(@"object_id")]
+ public string ObjectId { get; set; } = null!;
+
+ [JsonProperty(@"source_user_id")]
+ public long? SourceUserId { get; set; }
+
+ [JsonProperty(@"is_read")]
+ public bool IsRead { get; set; }
+
+ [JsonProperty(@"details")]
+ public JObject? Details { get; set; }
+ }
+}
diff --git a/osu.Game/Online/API/Requests/Responses/APINotificationsBundle.cs b/osu.Game/Online/API/Requests/Responses/APINotificationsBundle.cs
new file mode 100644
index 0000000000..ae299e2614
--- /dev/null
+++ b/osu.Game/Online/API/Requests/Responses/APINotificationsBundle.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Newtonsoft.Json;
+
+namespace osu.Game.Online.API.Requests.Responses
+{
+ [JsonObject(MemberSerialization.OptIn)]
+ public class APINotificationsBundle
+ {
+ [JsonProperty(@"has_more")]
+ public bool HasMore { get; set; }
+
+ [JsonProperty(@"notifications")]
+ public APINotification[] Notifications { get; set; } = null!;
+
+ [JsonProperty(@"notification_endpoint")]
+ public string Endpoint { get; set; } = null!;
+ }
+}
diff --git a/osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs b/osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs
new file mode 100644
index 0000000000..6ed22a19b2
--- /dev/null
+++ b/osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace osu.Game.Online.API.Requests.Responses
+{
+ [JsonObject(MemberSerialization.OptIn)]
+ public class ChatAckResponse
+ {
+ [JsonProperty("silences")]
+ public List Silences { get; set; } = null!;
+ }
+}
diff --git a/osu.Game/Online/API/Requests/Responses/ChatSilence.cs b/osu.Game/Online/API/Requests/Responses/ChatSilence.cs
new file mode 100644
index 0000000000..afb44e385e
--- /dev/null
+++ b/osu.Game/Online/API/Requests/Responses/ChatSilence.cs
@@ -0,0 +1,17 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Newtonsoft.Json;
+
+namespace osu.Game.Online.API.Requests.Responses
+{
+ [JsonObject(MemberSerialization.OptIn)]
+ public class ChatSilence
+ {
+ [JsonProperty("id")]
+ public uint Id { get; set; }
+
+ [JsonProperty("user_id")]
+ public int UserId { get; set; }
+ }
+}
diff --git a/osu.Game/Online/API/Requests/Responses/GetChannelResponse.cs b/osu.Game/Online/API/Requests/Responses/GetChannelResponse.cs
new file mode 100644
index 0000000000..24b886e74d
--- /dev/null
+++ b/osu.Game/Online/API/Requests/Responses/GetChannelResponse.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using osu.Game.Online.Chat;
+
+namespace osu.Game.Online.API.Requests.Responses
+{
+ [JsonObject(MemberSerialization.OptIn)]
+ public class GetChannelResponse
+ {
+ [JsonProperty(@"channel")]
+ public Channel Channel { get; set; } = null!;
+
+ [JsonProperty(@"users")]
+ public List Users { get; set; } = null!;
+ }
+}
diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
index 77dcfd39e3..15f4bace96 100644
--- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
+++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Online.API.Requests.Responses
public bool Passed { get; set; }
[JsonProperty("total_score")]
- public int TotalScore { get; set; }
+ public long TotalScore { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
@@ -213,7 +213,7 @@ namespace osu.Game.Online.API.Requests.Responses
public static SoloScoreInfo ForSubmission(ScoreInfo score) => new SoloScoreInfo
{
Rank = score.Rank,
- TotalScore = (int)score.TotalScore,
+ TotalScore = score.TotalScore,
Accuracy = score.Accuracy,
PP = score.PP,
MaxCombo = score.MaxCombo,
diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs
index f51ea3e8d6..24b384b1d4 100644
--- a/osu.Game/Online/Chat/Channel.cs
+++ b/osu.Game/Online/Chat/Channel.cs
@@ -134,6 +134,14 @@ namespace osu.Game.Online.Chat
///
public void AddNewMessages(params Message[] messages)
{
+ foreach (var m in messages)
+ {
+ LocalEchoMessage localEcho = pendingMessages.FirstOrDefault(local => local.Uuid == m.Uuid);
+
+ if (localEcho != null)
+ ReplaceMessage(localEcho, m);
+ }
+
messages = messages.Except(Messages).ToArray();
if (messages.Length == 0) return;
@@ -149,6 +157,20 @@ namespace osu.Game.Online.Chat
NewMessagesArrived?.Invoke(messages);
}
+ public void RemoveMessagesFromUser(int userId)
+ {
+ for (int i = 0; i < Messages.Count; i++)
+ {
+ var message = Messages[i];
+
+ if (message.SenderId == userId)
+ {
+ Messages.RemoveAt(i--);
+ MessageRemoved?.Invoke(message);
+ }
+ }
+ }
+
///
/// Replace or remove a message from the channel.
///
@@ -171,6 +193,10 @@ namespace osu.Game.Online.Chat
throw new InvalidOperationException("Attempted to add the same message again");
Messages.Add(final);
+
+ if (final.Id > LastMessageId)
+ LastMessageId = final.Id;
+
PendingMessageResolved?.Invoke(echo, final);
}
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index ec84b0643d..25a53360f0 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -6,16 +6,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
+using osu.Framework.Threading;
using osu.Game.Database;
-using osu.Game.Input;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Notifications;
using osu.Game.Overlays.Chat.Listing;
namespace osu.Game.Online.Chat
@@ -23,7 +24,7 @@ namespace osu.Game.Online.Chat
///
/// Manages everything channel related
///
- public class ChannelManager : PollingComponent, IChannelPostTarget
+ public class ChannelManager : CompositeComponent, IChannelPostTarget
{
///
/// The channels the player joins on startup
@@ -64,44 +65,50 @@ namespace osu.Game.Online.Chat
public IBindableList AvailableChannels => availableChannels;
private readonly IAPIProvider api;
+ private readonly NotificationsClientConnector connector;
[Resolved]
private UserLookupCache users { get; set; }
- public readonly BindableBool HighPollRate = new BindableBool();
+ private readonly IBindable apiState = new Bindable();
+ private bool channelsInitialised;
+ private ScheduledDelegate scheduledAck;
- private readonly IBindable isIdle = new BindableBool();
+ private long? lastSilenceMessageId;
+ private uint? lastSilenceId;
public ChannelManager(IAPIProvider api)
{
this.api = api;
+
+ connector = api.GetNotificationsConnector();
+
CurrentChannel.ValueChanged += currentChannelChanged;
}
- [BackgroundDependencyLoader(permitNulls: true)]
- private void load(IdleTracker idleTracker)
+ [BackgroundDependencyLoader]
+ private void load()
{
- HighPollRate.BindValueChanged(updatePollRate);
- isIdle.BindValueChanged(updatePollRate, true);
+ connector.ChannelJoined += ch => Schedule(() => joinChannel(ch));
- if (idleTracker != null)
- isIdle.BindTo(idleTracker.IsIdle);
- }
+ connector.ChannelParted += ch => Schedule(() => LeaveChannel(getChannel(ch)));
- private void updatePollRate(ValueChangedEvent valueChangedEvent)
- {
- // Polling will eventually be replaced with websocket, but let's avoid doing these background operations as much as possible for now.
- // The only loss will be delayed PM/message highlight notifications.
- int millisecondsBetweenPolls = HighPollRate.Value ? 1000 : 60000;
+ connector.NewMessages += msgs => Schedule(() => addMessages(msgs));
- if (isIdle.Value)
- millisecondsBetweenPolls *= 10;
-
- if (TimeBetweenPolls.Value != millisecondsBetweenPolls)
+ connector.PresenceReceived += () => Schedule(() =>
{
- TimeBetweenPolls.Value = millisecondsBetweenPolls;
- Logger.Log($"Chat is now polling every {TimeBetweenPolls.Value} ms");
- }
+ if (!channelsInitialised)
+ {
+ channelsInitialised = true;
+ // we want this to run after the first presence so we can see if the user is in any channels already.
+ initializeChannels();
+ }
+ });
+
+ connector.Start();
+
+ apiState.BindTo(api.State);
+ apiState.BindValueChanged(_ => SendAck(), true);
}
///
@@ -181,7 +188,8 @@ namespace osu.Game.Online.Chat
Timestamp = DateTimeOffset.Now,
ChannelId = target.Id,
IsAction = isAction,
- Content = text
+ Content = text,
+ Uuid = Guid.NewGuid().ToString()
};
target.AddLocalEcho(message);
@@ -191,13 +199,7 @@ namespace osu.Game.Online.Chat
{
var createNewPrivateMessageRequest = new CreateNewPrivateMessageRequest(target.Users.First(), message);
- createNewPrivateMessageRequest.Success += createRes =>
- {
- target.Id = createRes.ChannelID;
- target.ReplaceMessage(message, createRes.Message);
- dequeueAndRun();
- };
-
+ createNewPrivateMessageRequest.Success += _ => dequeueAndRun();
createNewPrivateMessageRequest.Failure += exception =>
{
handlePostException(exception);
@@ -211,12 +213,7 @@ namespace osu.Game.Online.Chat
var req = new PostMessageRequest(message);
- req.Success += m =>
- {
- target.ReplaceMessage(message, m);
- dequeueAndRun();
- };
-
+ req.Success += m => dequeueAndRun();
req.Failure += exception =>
{
handlePostException(exception);
@@ -328,12 +325,14 @@ namespace osu.Game.Online.Chat
}
}
- private void handleChannelMessages(IEnumerable messages)
+ private void addMessages(List messages)
{
var channels = JoinedChannels.ToList();
foreach (var group in messages.GroupBy(m => m.ChannelId))
channels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
+
+ lastSilenceMessageId ??= messages.LastOrDefault()?.Id;
}
private void initializeChannels()
@@ -376,13 +375,51 @@ namespace osu.Game.Online.Chat
var fetchInitialMsgReq = new GetMessagesRequest(channel);
fetchInitialMsgReq.Success += messages =>
{
- handleChannelMessages(messages);
+ addMessages(messages);
channel.MessagesLoaded = true; // this will mark the channel as having received messages even if there were none.
};
api.Queue(fetchInitialMsgReq);
}
+ ///
+ /// Sends an acknowledgement request to the API.
+ /// This marks the user as online to receive messages from public channels, while also returning a list of silenced users.
+ /// It needs to be called at least once every 10 minutes to remain visibly marked as online.
+ ///
+ public void SendAck()
+ {
+ if (apiState.Value != APIState.Online)
+ return;
+
+ var req = new ChatAckRequest
+ {
+ SinceMessageId = lastSilenceMessageId,
+ SinceSilenceId = lastSilenceId
+ };
+
+ req.Failure += _ => scheduleNextRequest();
+ req.Success += ack =>
+ {
+ foreach (var silence in ack.Silences)
+ {
+ foreach (var channel in JoinedChannels)
+ channel.RemoveMessagesFromUser(silence.UserId);
+ lastSilenceId = Math.Max(lastSilenceId ?? 0, silence.Id);
+ }
+
+ scheduleNextRequest();
+ };
+
+ api.Queue(req);
+
+ void scheduleNextRequest()
+ {
+ scheduledAck?.Cancel();
+ scheduledAck = Scheduler.AddDelayed(SendAck, 60000);
+ }
+ }
+
///
/// Find an existing channel instance for the provided channel. Lookup is performed basd on ID.
/// The provided channel may be used if an existing instance is not found.
@@ -395,7 +432,13 @@ namespace osu.Game.Online.Chat
{
Channel found = null;
- bool lookupCondition(Channel ch) => lookup.Id > 0 ? ch.Id == lookup.Id : lookup.Name == ch.Name;
+ bool lookupCondition(Channel ch)
+ {
+ if (ch.Id > 0 && lookup.Id > 0)
+ return ch.Id == lookup.Id;
+
+ return ch.Name == lookup.Name;
+ }
var available = AvailableChannels.FirstOrDefault(lookupCondition);
if (available != null)
@@ -415,6 +458,12 @@ namespace osu.Game.Online.Chat
if (foundSelf != null)
found.Users.Remove(foundSelf);
}
+ else
+ {
+ found.Id = lookup.Id;
+ found.Name = lookup.Name;
+ found.LastMessageId = Math.Max(found.LastMessageId ?? 0, lookup.LastMessageId ?? 0);
+ }
if (joined == null && addToJoined) joinedChannels.Add(found);
if (available == null && addToAvailable) availableChannels.Add(found);
@@ -464,7 +513,7 @@ namespace osu.Game.Online.Chat
{
channel.Id = resChannel.ChannelID.Value;
- handleChannelMessages(resChannel.RecentMessages);
+ addMessages(resChannel.RecentMessages);
channel.MessagesLoaded = true; // this will mark the channel as having received messages even if there were none.
}
};
@@ -574,57 +623,6 @@ namespace osu.Game.Online.Chat
}
}
- private long lastMessageId;
-
- private bool channelsInitialised;
-
- protected override Task Poll()
- {
- if (!api.IsLoggedIn)
- return base.Poll();
-
- var fetchReq = new GetUpdatesRequest(lastMessageId);
-
- var tcs = new TaskCompletionSource();
-
- fetchReq.Success += updates =>
- {
- if (updates?.Presence != null)
- {
- foreach (var channel in updates.Presence)
- {
- // we received this from the server so should mark the channel already joined.
- channel.Joined.Value = true;
- joinChannel(channel);
- }
-
- //todo: handle left channels
-
- handleChannelMessages(updates.Messages);
-
- foreach (var group in updates.Messages.GroupBy(m => m.ChannelId))
- JoinedChannels.FirstOrDefault(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
-
- lastMessageId = updates.Messages.LastOrDefault()?.Id ?? lastMessageId;
- }
-
- if (!channelsInitialised)
- {
- channelsInitialised = true;
- // we want this to run after the first presence so we can see if the user is in any channels already.
- initializeChannels();
- }
-
- tcs.SetResult(true);
- };
-
- fetchReq.Failure += _ => tcs.SetResult(false);
-
- api.Queue(fetchReq);
-
- return tcs.Task;
- }
-
///
/// Marks the as read
///
@@ -646,6 +644,12 @@ namespace osu.Game.Online.Chat
api.Queue(req);
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ connector?.Dispose();
+ }
}
///
diff --git a/osu.Game/Online/Chat/Message.cs b/osu.Game/Online/Chat/Message.cs
index 86562341eb..9f6f9c8d6b 100644
--- a/osu.Game/Online/Chat/Message.cs
+++ b/osu.Game/Online/Chat/Message.cs
@@ -30,6 +30,19 @@ namespace osu.Game.Online.Chat
[JsonProperty(@"sender")]
public APIUser Sender;
+ [JsonProperty(@"sender_id")]
+ public int SenderId
+ {
+ get => Sender?.Id ?? 0;
+ set => Sender = new APIUser { Id = value };
+ }
+
+ ///
+ /// A unique identifier for this message. Sent to and from osu!web to use for deduplication.
+ ///
+ [JsonProperty(@"uuid")]
+ public string Uuid { get; set; } = string.Empty;
+
[JsonConstructor]
public Message()
{
diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs
index 6f246f6dd3..ca6d2932f7 100644
--- a/osu.Game/Online/HubClientConnector.cs
+++ b/osu.Game/Online/HubClientConnector.cs
@@ -49,6 +49,9 @@ namespace osu.Game.Online
this.api = api;
this.versionHash = versionHash;
this.preferMessagePack = preferMessagePack;
+
+ // Automatically start these connections.
+ Start();
}
protected override Task BuildConnectionAsync(CancellationToken cancellationToken)
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 75334952f0..b39781ebc2 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Development;
@@ -727,13 +728,20 @@ namespace osu.Game.Online.Multiplayer
if (Room == null)
return;
- Debug.Assert(APIRoom != null);
+ try
+ {
+ Debug.Assert(APIRoom != null);
- Room.Playlist[Room.Playlist.IndexOf(Room.Playlist.Single(existing => existing.ID == item.ID))] = item;
+ Room.Playlist[Room.Playlist.IndexOf(Room.Playlist.Single(existing => existing.ID == item.ID))] = item;
- int existingIndex = APIRoom.Playlist.IndexOf(APIRoom.Playlist.Single(existing => existing.ID == item.ID));
- APIRoom.Playlist.RemoveAt(existingIndex);
- APIRoom.Playlist.Insert(existingIndex, createPlaylistItem(item));
+ int existingIndex = APIRoom.Playlist.IndexOf(APIRoom.Playlist.Single(existing => existing.ID == item.ID));
+ APIRoom.Playlist.RemoveAt(existingIndex);
+ APIRoom.Playlist.Insert(existingIndex, createPlaylistItem(item));
+ }
+ catch (Exception ex)
+ {
+ throw new AggregateException($"Item: {JsonConvert.SerializeObject(createPlaylistItem(item))}\n\nRoom:{JsonConvert.SerializeObject(APIRoom)}", ex);
+ }
ItemChanged?.Invoke(item);
RoomUpdated?.Invoke();
diff --git a/osu.Game/Online/Notifications/NotificationsClient.cs b/osu.Game/Online/Notifications/NotificationsClient.cs
new file mode 100644
index 0000000000..6198707111
--- /dev/null
+++ b/osu.Game/Online/Notifications/NotificationsClient.cs
@@ -0,0 +1,76 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.Chat;
+
+namespace osu.Game.Online.Notifications
+{
+ ///
+ /// An abstract client which receives notification-related events (chat/notifications).
+ ///
+ public abstract class NotificationsClient : PersistentEndpointClient
+ {
+ public Action? ChannelJoined;
+ public Action? ChannelParted;
+ public Action>? NewMessages;
+ public Action? PresenceReceived;
+
+ protected readonly IAPIProvider API;
+
+ private long lastMessageId;
+
+ protected NotificationsClient(IAPIProvider api)
+ {
+ API = api;
+ }
+
+ public override Task ConnectAsync(CancellationToken cancellationToken)
+ {
+ API.Queue(CreateFetchMessagesRequest(0));
+ return Task.CompletedTask;
+ }
+
+ protected APIRequest CreateFetchMessagesRequest(long? lastMessageId = null)
+ {
+ var fetchReq = new GetUpdatesRequest(lastMessageId ?? this.lastMessageId);
+
+ fetchReq.Success += updates =>
+ {
+ if (updates?.Presence != null)
+ {
+ foreach (var channel in updates.Presence)
+ HandleChannelJoined(channel);
+
+ //todo: handle left channels
+
+ HandleMessages(updates.Messages);
+ }
+
+ PresenceReceived?.Invoke();
+ };
+
+ return fetchReq;
+ }
+
+ protected void HandleChannelJoined(Channel channel)
+ {
+ channel.Joined.Value = true;
+ ChannelJoined?.Invoke(channel);
+ }
+
+ protected void HandleChannelParted(Channel channel) => ChannelParted?.Invoke(channel);
+
+ protected void HandleMessages(List messages)
+ {
+ NewMessages?.Invoke(messages);
+ lastMessageId = Math.Max(lastMessageId, messages.LastOrDefault()?.Id ?? 0);
+ }
+ }
+}
diff --git a/osu.Game/Online/Notifications/NotificationsClientConnector.cs b/osu.Game/Online/Notifications/NotificationsClientConnector.cs
new file mode 100644
index 0000000000..d2c2e6673c
--- /dev/null
+++ b/osu.Game/Online/Notifications/NotificationsClientConnector.cs
@@ -0,0 +1,42 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using osu.Game.Online.API;
+using osu.Game.Online.Chat;
+
+namespace osu.Game.Online.Notifications
+{
+ ///
+ /// An abstract connector or s.
+ ///
+ public abstract class NotificationsClientConnector : PersistentEndpointClientConnector
+ {
+ public event Action? ChannelJoined;
+ public event Action? ChannelParted;
+ public event Action>? NewMessages;
+ public event Action? PresenceReceived;
+
+ protected NotificationsClientConnector(IAPIProvider api)
+ : base(api)
+ {
+ }
+
+ protected sealed override async Task BuildConnectionAsync(CancellationToken cancellationToken)
+ {
+ var client = await BuildNotificationClientAsync(cancellationToken);
+
+ client.ChannelJoined = c => ChannelJoined?.Invoke(c);
+ client.ChannelParted = c => ChannelParted?.Invoke(c);
+ client.NewMessages = m => NewMessages?.Invoke(m);
+ client.PresenceReceived = () => PresenceReceived?.Invoke();
+
+ return client;
+ }
+
+ protected abstract Task BuildNotificationClientAsync(CancellationToken cancellationToken);
+ }
+}
diff --git a/osu.Game/Online/Notifications/WebSocket/EndChatRequest.cs b/osu.Game/Online/Notifications/WebSocket/EndChatRequest.cs
new file mode 100644
index 0000000000..7f67587f5d
--- /dev/null
+++ b/osu.Game/Online/Notifications/WebSocket/EndChatRequest.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Newtonsoft.Json;
+
+namespace osu.Game.Online.Notifications.WebSocket
+{
+ ///
+ /// A websocket message notifying the server that the client no longer wants to receive chat messages.
+ ///
+ [JsonObject(MemberSerialization.OptIn)]
+ public class EndChatRequest : SocketMessage
+ {
+ public EndChatRequest()
+ {
+ Event = @"chat.end";
+ }
+ }
+}
diff --git a/osu.Game/Online/Notifications/WebSocket/NewChatMessageData.cs b/osu.Game/Online/Notifications/WebSocket/NewChatMessageData.cs
new file mode 100644
index 0000000000..850fbd226b
--- /dev/null
+++ b/osu.Game/Online/Notifications/WebSocket/NewChatMessageData.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+using Newtonsoft.Json;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Chat;
+
+namespace osu.Game.Online.Notifications.WebSocket
+{
+ ///
+ /// A websocket message sent from the server when new messages arrive.
+ ///
+ [JsonObject(MemberSerialization.OptIn)]
+ public class NewChatMessageData
+ {
+ [JsonProperty(@"messages")]
+ public List Messages { get; set; } = null!;
+
+ [JsonProperty(@"users")]
+ private List users { get; set; } = null!;
+
+ [OnDeserialized]
+ private void onDeserialised(StreamingContext context)
+ {
+ foreach (var m in Messages)
+ m.Sender = users.Single(u => u.OnlineID == m.SenderId);
+ }
+ }
+}
diff --git a/osu.Game/Online/Notifications/WebSocket/SocketMessage.cs b/osu.Game/Online/Notifications/WebSocket/SocketMessage.cs
new file mode 100644
index 0000000000..666a9dd8a3
--- /dev/null
+++ b/osu.Game/Online/Notifications/WebSocket/SocketMessage.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace osu.Game.Online.Notifications.WebSocket
+{
+ ///
+ /// A websocket message, sent either from the client or server.
+ ///
+ [JsonObject(MemberSerialization.OptIn)]
+ public class SocketMessage
+ {
+ [JsonProperty(@"event")]
+ public string Event { get; set; } = null!;
+
+ [JsonProperty(@"data")]
+ public JObject? Data { get; set; }
+
+ [JsonProperty(@"error")]
+ public string? Error { get; set; }
+ }
+}
diff --git a/osu.Game/Online/Notifications/WebSocket/StartChatRequest.cs b/osu.Game/Online/Notifications/WebSocket/StartChatRequest.cs
new file mode 100644
index 0000000000..9dd69a7377
--- /dev/null
+++ b/osu.Game/Online/Notifications/WebSocket/StartChatRequest.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Newtonsoft.Json;
+
+namespace osu.Game.Online.Notifications.WebSocket
+{
+ ///
+ /// A websocket message notifying the server that the client wants to receive chat messages.
+ ///
+ [JsonObject(MemberSerialization.OptIn)]
+ public class StartChatRequest : SocketMessage
+ {
+ public StartChatRequest()
+ {
+ Event = @"chat.start";
+ }
+ }
+}
diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs
new file mode 100644
index 0000000000..d8d78297e3
--- /dev/null
+++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs
@@ -0,0 +1,179 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Net.WebSockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using osu.Framework.Extensions.TypeExtensions;
+using osu.Framework.Logging;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.Chat;
+
+namespace osu.Game.Online.Notifications.WebSocket
+{
+ ///
+ /// A notifications client which receives events via a websocket.
+ ///
+ public class WebSocketNotificationsClient : NotificationsClient
+ {
+ private readonly ClientWebSocket socket;
+ private readonly string endpoint;
+ private readonly ConcurrentDictionary channelsMap = new ConcurrentDictionary();
+
+ public WebSocketNotificationsClient(ClientWebSocket socket, string endpoint, IAPIProvider api)
+ : base(api)
+ {
+ this.socket = socket;
+ this.endpoint = endpoint;
+ }
+
+ public override async Task ConnectAsync(CancellationToken cancellationToken)
+ {
+ await socket.ConnectAsync(new Uri(endpoint), cancellationToken).ConfigureAwait(false);
+ await sendMessage(new StartChatRequest(), CancellationToken.None);
+
+ runReadLoop(cancellationToken);
+
+ await base.ConnectAsync(cancellationToken);
+ }
+
+ private void runReadLoop(CancellationToken cancellationToken) => Task.Run(async () =>
+ {
+ byte[] buffer = new byte[1024];
+ StringBuilder messageResult = new StringBuilder();
+
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, cancellationToken);
+
+ switch (result.MessageType)
+ {
+ case WebSocketMessageType.Text:
+ messageResult.Append(Encoding.UTF8.GetString(buffer[..result.Count]));
+
+ if (result.EndOfMessage)
+ {
+ SocketMessage? message = JsonConvert.DeserializeObject(messageResult.ToString());
+ messageResult.Clear();
+
+ Debug.Assert(message != null);
+
+ if (message.Error != null)
+ {
+ Logger.Log($"{GetType().ReadableName()} error: {message.Error}", LoggingTarget.Network);
+ break;
+ }
+
+ await onMessageReceivedAsync(message);
+ }
+
+ break;
+
+ case WebSocketMessageType.Binary:
+ throw new NotImplementedException("Binary message type not supported.");
+
+ case WebSocketMessageType.Close:
+ throw new Exception("Connection closed by remote host.");
+ }
+ }
+ catch (Exception ex)
+ {
+ await InvokeClosed(ex);
+ return;
+ }
+ }
+ }, cancellationToken);
+
+ private async Task closeAsync()
+ {
+ try
+ {
+ await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, @"Disconnecting", CancellationToken.None).ConfigureAwait(false);
+ }
+ catch
+ {
+ // Closure can fail if the connection is aborted. Don't really care since it's disposed anyway.
+ }
+ }
+
+ private async Task sendMessage(SocketMessage message, CancellationToken cancellationToken)
+ {
+ if (socket.State != WebSocketState.Open)
+ return;
+
+ await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken);
+ }
+
+ private async Task onMessageReceivedAsync(SocketMessage message)
+ {
+ switch (message.Event)
+ {
+ case @"chat.channel.join":
+ Debug.Assert(message.Data != null);
+
+ Channel? joinedChannel = JsonConvert.DeserializeObject(message.Data.ToString());
+ Debug.Assert(joinedChannel != null);
+
+ HandleChannelJoined(joinedChannel);
+ break;
+
+ case @"chat.channel.part":
+ Debug.Assert(message.Data != null);
+
+ Channel? partedChannel = JsonConvert.DeserializeObject(message.Data.ToString());
+ Debug.Assert(partedChannel != null);
+
+ HandleChannelParted(partedChannel);
+ break;
+
+ case @"chat.message.new":
+ Debug.Assert(message.Data != null);
+
+ NewChatMessageData? messageData = JsonConvert.DeserializeObject(message.Data.ToString());
+ Debug.Assert(messageData != null);
+
+ foreach (var msg in messageData.Messages)
+ HandleChannelJoined(await getChannel(msg.ChannelId));
+
+ HandleMessages(messageData.Messages);
+ break;
+ }
+ }
+
+ private async Task getChannel(long channelId)
+ {
+ if (channelsMap.TryGetValue(channelId, out Channel channel))
+ return channel;
+
+ var tsc = new TaskCompletionSource();
+ var req = new GetChannelRequest(channelId);
+
+ req.Success += response =>
+ {
+ channelsMap[channelId] = response.Channel;
+ tsc.SetResult(response.Channel);
+ };
+
+ req.Failure += ex => tsc.SetException(ex);
+
+ API.Queue(req);
+
+ return await tsc.Task;
+ }
+
+ public override async ValueTask DisposeAsync()
+ {
+ await base.DisposeAsync();
+ await closeAsync();
+ socket.Dispose();
+ }
+ }
+}
diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs
new file mode 100644
index 0000000000..21335a3b59
--- /dev/null
+++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Net;
+using System.Net.WebSockets;
+using System.Threading;
+using System.Threading.Tasks;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+
+namespace osu.Game.Online.Notifications.WebSocket
+{
+ ///
+ /// A connector for s that receive events via a websocket.
+ ///
+ public class WebSocketNotificationsClientConnector : NotificationsClientConnector
+ {
+ private readonly IAPIProvider api;
+
+ public WebSocketNotificationsClientConnector(IAPIProvider api)
+ : base(api)
+ {
+ this.api = api;
+ }
+
+ protected override async Task BuildNotificationClientAsync(CancellationToken cancellationToken)
+ {
+ var tcs = new TaskCompletionSource();
+
+ var req = new GetNotificationsRequest();
+ req.Success += bundle => tcs.SetResult(bundle.Endpoint);
+ req.Failure += ex => tcs.SetException(ex);
+ api.Queue(req);
+
+ string endpoint = await tcs.Task;
+
+ ClientWebSocket socket = new ClientWebSocket();
+ socket.Options.SetRequestHeader(@"Authorization", @$"Bearer {api.AccessToken}");
+ socket.Options.Proxy = WebRequest.DefaultWebProxy;
+ if (socket.Options.Proxy != null)
+ socket.Options.Proxy.Credentials = CredentialCache.DefaultCredentials;
+
+ return new WebSocketNotificationsClient(socket, endpoint, api);
+ }
+ }
+}
diff --git a/osu.Game/Online/PersistentEndpointClientConnector.cs b/osu.Game/Online/PersistentEndpointClientConnector.cs
index 70e10c6c7d..be76644745 100644
--- a/osu.Game/Online/PersistentEndpointClientConnector.cs
+++ b/osu.Game/Online/PersistentEndpointClientConnector.cs
@@ -23,11 +23,13 @@ namespace osu.Game.Online
///
public PersistentEndpointClient? CurrentConnection { get; private set; }
+ protected readonly IAPIProvider API;
+
+ private readonly IBindable apiState = new Bindable();
private readonly Bindable isConnected = new Bindable();
private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1);
private CancellationTokenSource connectCancelSource = new CancellationTokenSource();
-
- private readonly IBindable apiState = new Bindable();
+ private bool started;
///
/// Constructs a new .
@@ -35,8 +37,20 @@ namespace osu.Game.Online
/// An API provider used to react to connection state changes.
protected PersistentEndpointClientConnector(IAPIProvider api)
{
+ API = api;
apiState.BindTo(api.State);
+ }
+
+ ///
+ /// Attempts to connect and begins processing messages from the remote endpoint.
+ ///
+ public void Start()
+ {
+ if (started)
+ return;
+
apiState.BindValueChanged(_ => Task.Run(connectIfPossible), true);
+ started = true;
}
public Task Reconnect()
@@ -131,7 +145,9 @@ namespace osu.Game.Online
private async Task onConnectionClosed(Exception? ex, CancellationToken cancellationToken)
{
- isConnected.Value = false;
+ bool hasBeenCancelled = cancellationToken.IsCancellationRequested;
+
+ await disconnect(true);
if (ex != null)
await handleErrorAndDelay(ex, cancellationToken).ConfigureAwait(false);
@@ -139,7 +155,7 @@ namespace osu.Game.Online
Logger.Log($"{ClientName} disconnected", LoggingTarget.Network);
// make sure a disconnect wasn't triggered (and this is still the active connection).
- if (!cancellationToken.IsCancellationRequested)
+ if (!hasBeenCancelled)
await Task.Run(connect, default).ConfigureAwait(false);
}
@@ -160,7 +176,9 @@ namespace osu.Game.Online
}
finally
{
+ isConnected.Value = false;
CurrentConnection = null;
+
if (takeLock)
connectionLock.Release();
}
diff --git a/osu.Game/Online/Rooms/IndexedMultiplayerScores.cs b/osu.Game/Online/Rooms/IndexedMultiplayerScores.cs
index 459602f1b4..59cba2340d 100644
--- a/osu.Game/Online/Rooms/IndexedMultiplayerScores.cs
+++ b/osu.Game/Online/Rooms/IndexedMultiplayerScores.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Online.Rooms
/// The total scores in the playlist item.
///
[JsonProperty("total")]
- public int? TotalScores { get; set; }
+ public long? TotalScores { get; set; }
///
/// The user's score, if any.
diff --git a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
index 87f25874c5..573c504add 100644
--- a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
+++ b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Online.Spectator
///
/// The current total score.
///
- public readonly BindableDouble TotalScore = new BindableDouble { MinValue = 0 };
+ public readonly BindableLong TotalScore = new BindableLong { MinValue = 0 };
///
/// The current accuracy.
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 7476324e11..a93c187e53 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -910,19 +910,6 @@ namespace osu.Game
loadComponentSingleFile(new BackgroundBeatmapProcessor(), Add);
- chatOverlay.State.BindValueChanged(_ => updateChatPollRate());
- // Multiplayer modes need to increase poll rate temporarily.
- API.Activity.BindValueChanged(_ => updateChatPollRate(), true);
-
- void updateChatPollRate()
- {
- channelManager.HighPollRate.Value =
- chatOverlay.State.Value == Visibility.Visible
- || API.Activity.Value is UserActivity.InLobby
- || API.Activity.Value is UserActivity.InMultiplayerGame
- || API.Activity.Value is UserActivity.SpectatingMultiplayerGame;
- }
-
Add(difficultyRecommender);
Add(externalLinkOpener = new ExternalLinkOpener());
Add(new MusicKeyBindingHandler());
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index c278c9cb93..2d9583b864 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -41,11 +41,8 @@ namespace osu.Game.Overlays
private IBindable apiUser;
- private Drawable currentContent;
private Container panelTarget;
private FillFlowContainer foundContent;
- private NotFoundDrawable notFoundContent;
- private SupporterRequiredDrawable supporterRequiredContent;
private BeatmapListingFilterControl filterControl;
public BeatmapListingOverlay()
@@ -86,11 +83,6 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.X,
Masking = true,
Padding = new MarginPadding { Horizontal = 20 },
- Children = new Drawable[]
- {
- notFoundContent = new NotFoundDrawable(),
- supporterRequiredContent = new SupporterRequiredDrawable(),
- }
}
},
},
@@ -107,7 +99,7 @@ namespace osu.Game.Overlays
apiUser.BindValueChanged(_ => Schedule(() =>
{
if (api.IsLoggedIn)
- addContentToResultsArea(Drawable.Empty());
+ replaceResultsAreaContent(Drawable.Empty());
}));
}
@@ -155,8 +147,8 @@ namespace osu.Game.Overlays
if (searchResult.Type == BeatmapListingFilterControl.SearchResultType.SupporterOnlyFilters)
{
- supporterRequiredContent.UpdateText(searchResult.SupporterOnlyFiltersUsed);
- addContentToResultsArea(supporterRequiredContent);
+ var supporterOnly = new SupporterRequiredDrawable(searchResult.SupporterOnlyFiltersUsed);
+ replaceResultsAreaContent(supporterOnly);
return;
}
@@ -167,13 +159,13 @@ namespace osu.Game.Overlays
//No matches case
if (!newCards.Any())
{
- addContentToResultsArea(notFoundContent);
+ replaceResultsAreaContent(new NotFoundDrawable());
return;
}
var content = createCardContainerFor(newCards);
- panelLoadTask = LoadComponentAsync(foundContent = content, addContentToResultsArea, (cancellationToken = new CancellationTokenSource()).Token);
+ panelLoadTask = LoadComponentAsync(foundContent = content, replaceResultsAreaContent, (cancellationToken = new CancellationTokenSource()).Token);
}
else
{
@@ -221,36 +213,16 @@ namespace osu.Game.Overlays
return content;
}
- private void addContentToResultsArea(Drawable content)
+ private void replaceResultsAreaContent(Drawable content)
{
Loading.Hide();
lastFetchDisplayedTime = Time.Current;
- if (content == currentContent)
- return;
-
- var lastContent = currentContent;
-
- if (lastContent != null)
- {
- lastContent.FadeOut();
- if (!isPlaceholderContent(lastContent))
- lastContent.Expire();
- }
-
- if (!content.IsAlive)
- panelTarget.Add(content);
+ panelTarget.Child = content;
content.FadeInFromZero();
- currentContent = content;
}
- ///
- /// Whether is a static placeholder reused multiple times by this overlay.
- ///
- private bool isPlaceholderContent(Drawable drawable)
- => drawable == notFoundContent || drawable == supporterRequiredContent;
-
private void onCardSizeChanged()
{
if (foundContent?.IsAlive != true || !foundContent.Any())
@@ -287,7 +259,7 @@ namespace osu.Game.Overlays
}
[BackgroundDependencyLoader]
- private void load(TextureStore textures)
+ private void load(LargeTextureStore textures)
{
AddInternal(new FillFlowContainer
{
@@ -324,15 +296,19 @@ namespace osu.Game.Overlays
{
private LinkFlowContainer supporterRequiredText;
- public SupporterRequiredDrawable()
+ private readonly List filtersUsed;
+
+ public SupporterRequiredDrawable(List filtersUsed)
{
RelativeSizeAxes = Axes.X;
Height = 225;
Alpha = 0;
+
+ this.filtersUsed = filtersUsed;
}
[BackgroundDependencyLoader]
- private void load(TextureStore textures)
+ private void load(LargeTextureStore textures)
{
AddInternal(new FillFlowContainer
{
@@ -360,14 +336,9 @@ namespace osu.Game.Overlays
},
}
});
- }
-
- public void UpdateText(List filters)
- {
- supporterRequiredText.Clear();
supporterRequiredText.AddText(
- BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(string.Join(" and ", filters), "").ToString(),
+ BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(string.Join(" and ", filtersUsed), "").ToString(),
t =>
{
t.Font = OsuFont.GetFont(size: 16);
diff --git a/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs
index 8f5c942b6e..1d904526fd 100644
--- a/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs
+++ b/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs
@@ -119,22 +119,17 @@ namespace osu.Game.Overlays.Dashboard.Home.News
[BackgroundDependencyLoader]
private void load(GameHost host)
{
- NewsPostBackground bg;
-
- Child = new DelayedLoadWrapper(bg = new NewsPostBackground(post.FirstImage)
+ Child = new DelayedLoadUnloadWrapper(() => new NewsPostBackground(post.FirstImage)
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Alpha = 0
})
{
RelativeSizeAxes = Axes.Both
};
- bg.OnLoadComplete += d => d.FadeIn(250, Easing.In);
-
TooltipText = "view in browser";
Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug);
diff --git a/osu.Game/Overlays/News/NewsCard.cs b/osu.Game/Overlays/News/NewsCard.cs
index eb76522e11..c8e0b0c7ef 100644
--- a/osu.Game/Overlays/News/NewsCard.cs
+++ b/osu.Game/Overlays/News/NewsCard.cs
@@ -49,7 +49,6 @@ namespace osu.Game.Overlays.News
Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug);
}
- NewsPostBackground bg;
AddRange(new Drawable[]
{
background = new Box
@@ -71,14 +70,14 @@ namespace osu.Game.Overlays.News
CornerRadius = 6,
Children = new Drawable[]
{
- new DelayedLoadWrapper(bg = new NewsPostBackground(post.FirstImage)
+ new DelayedLoadUnloadWrapper(() => new NewsPostBackground(post.FirstImage)
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0
- })
+ }, timeBeforeUnload: 5000)
{
RelativeSizeAxes = Axes.Both
},
@@ -116,8 +115,6 @@ namespace osu.Game.Overlays.News
IdleColour = colourProvider.Background4;
HoverColour = colourProvider.Background3;
- bg.OnLoadComplete += d => d.FadeIn(250, Easing.In);
-
main.AddParagraph(post.Title, t => t.Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold));
main.AddParagraph(post.Preview, t => t.Font = OsuFont.GetFont(size: 12)); // Should use sans-serif font
main.AddParagraph("by ", t => t.Font = OsuFont.GetFont(size: 12));
diff --git a/osu.Game/Overlays/News/NewsPostBackground.cs b/osu.Game/Overlays/News/NewsPostBackground.cs
index bddca8f7ec..b77623842c 100644
--- a/osu.Game/Overlays/News/NewsPostBackground.cs
+++ b/osu.Game/Overlays/News/NewsPostBackground.cs
@@ -4,6 +4,7 @@
#nullable disable
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
@@ -25,6 +26,12 @@ namespace osu.Game.Overlays.News
Texture = store.Get(createUrl(sourceUrl));
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ this.FadeInFromZero(500, Easing.OutQuint);
+ }
+
private string createUrl(string source)
{
if (string.IsNullOrEmpty(source))
diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs
index 900b4bebf0..949f1e7b96 100644
--- a/osu.Game/Overlays/NowPlayingOverlay.cs
+++ b/osu.Game/Overlays/NowPlayingOverlay.cs
@@ -100,7 +100,7 @@ namespace osu.Game.Overlays
},
Children = new[]
{
- background = new Background(),
+ background = Empty(),
title = new OsuSpriteText
{
Origin = Anchor.BottomCentre,
@@ -413,7 +413,7 @@ namespace osu.Game.Overlays
}
[BackgroundDependencyLoader]
- private void load(TextureStore textures)
+ private void load(LargeTextureStore textures)
{
sprite.Texture = beatmap?.Background ?? textures.Get(@"Backgrounds/bg4");
}
diff --git a/osu.Game/Overlays/OverlayHeaderBackground.cs b/osu.Game/Overlays/OverlayHeaderBackground.cs
index c47f16272f..540b28d9b2 100644
--- a/osu.Game/Overlays/OverlayHeaderBackground.cs
+++ b/osu.Game/Overlays/OverlayHeaderBackground.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Overlays
Height = 80;
RelativeSizeAxes = Axes.X;
Masking = true;
- InternalChild = new Background(textureName);
+ InternalChild = new DelayedLoadWrapper(() => new Background(textureName));
}
private class Background : Sprite
@@ -36,10 +36,16 @@ namespace osu.Game.Overlays
}
[BackgroundDependencyLoader]
- private void load(TextureStore textures)
+ private void load(LargeTextureStore textures)
{
Texture = textures.Get(textureName);
}
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ this.FadeInFromZero(500, Easing.OutQuint);
+ }
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs
index b92746a65a..2f4840a384 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs
@@ -3,6 +3,8 @@
#nullable disable
+using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Localisation;
@@ -13,6 +15,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
protected override LocalisableString Header => BindingSettingsStrings.ShortcutAndGameplayBindings;
+ public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "keybindings" });
+
public BindingSettings(KeyBindingPanel keyConfig)
{
Children = new Drawable[]
diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
index c91a6a48d4..25ab8cfad2 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
@@ -33,6 +33,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
public class KeyBindingRow : Container, IFilterable
{
+ ///
+ /// Invoked when the binding of this row is updated with a change being written.
+ ///
+ public Action BindingUpdated { get; set; }
+
private readonly object action;
private readonly IEnumerable bindings;
@@ -153,7 +158,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
Spacing = new Vector2(5),
Children = new Drawable[]
{
- new CancelButton { Action = finalise },
+ new CancelButton { Action = () => finalise(false) },
new ClearButton { Action = clear },
},
},
@@ -226,7 +231,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
}
}
- bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState));
+ bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromMouseButton(e.Button));
return true;
}
@@ -240,7 +245,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
}
if (bindTarget.IsHovered)
- finalise();
+ finalise(false);
// prevent updating bind target before clear button's action
else if (!cancelAndClearButtons.Any(b => b.IsHovered))
updateBindTarget();
@@ -252,7 +257,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
if (bindTarget.IsHovered)
{
- bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState, e.ScrollDelta));
+ bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState, e.ScrollDelta), KeyCombination.FromScrollDelta(e.ScrollDelta).First());
finalise();
return true;
}
@@ -263,10 +268,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input
protected override bool OnKeyDown(KeyDownEvent e)
{
- if (!HasFocus)
+ if (!HasFocus || e.Repeat)
return false;
- bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState));
+ bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromKey(e.Key));
if (!isModifier(e.Key)) finalise();
return true;
@@ -288,7 +293,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (!HasFocus)
return false;
- bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState));
+ bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromJoystickButton(e.Button));
finalise();
return true;
@@ -310,7 +315,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (!HasFocus)
return false;
- bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState));
+ bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromMidiKey(e.Key));
finalise();
return true;
@@ -332,7 +337,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (!HasFocus)
return false;
- bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState));
+ bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromTabletAuxiliaryButton(e.Button));
finalise();
return true;
@@ -354,7 +359,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (!HasFocus)
return false;
- bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState));
+ bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromTabletPenButton(e.Button));
finalise();
return true;
@@ -377,10 +382,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input
return;
bindTarget.UpdateKeyCombination(InputKey.None);
- finalise();
+ finalise(false);
}
- private void finalise()
+ private void finalise(bool hasChanged = true)
{
if (bindTarget != null)
{
@@ -393,6 +398,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
// schedule to ensure we don't instantly get focus back on next OnMouseClick (see AcceptFocus impl.)
bindTarget = null;
+ if (hasChanged)
+ BindingUpdated?.Invoke(this);
});
}
@@ -417,7 +424,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
protected override void OnFocusLost(FocusLostEvent e)
{
- finalise();
+ finalise(false);
base.OnFocusLost(e);
}
@@ -563,6 +570,14 @@ namespace osu.Game.Overlays.Settings.Sections.Input
}
}
+ ///
+ /// Update from a key combination, only allowing a single non-modifier key to be specified.
+ ///
+ /// A generated from the full input state.
+ /// The key which triggered this update, and should be used as the binding.
+ public void UpdateKeyCombination(KeyCombination fullState, InputKey triggerKey) =>
+ UpdateKeyCombination(new KeyCombination(fullState.Keys.Where(KeyCombination.IsModifierKey).Append(triggerKey)));
+
public void UpdateKeyCombination(KeyCombination newCombination)
{
if (KeyBinding.RulesetName != null && !RealmKeyBindingStore.CheckValidForGameplay(newCombination))
diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs
index 628fe08607..98d569948f 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs
@@ -19,6 +19,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
public abstract class KeyBindingsSubsection : SettingsSubsection
{
+ ///
+ /// After a successful binding, automatically select the next binding row to make quickly
+ /// binding a large set of keys easier on the user.
+ ///
+ protected virtual bool AutoAdvanceTarget => false;
+
protected IEnumerable Defaults;
public RulesetInfo Ruleset { get; protected set; }
@@ -49,7 +55,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.ActionInt.Equals(intKey)).ToList())
{
AllowMainMouseButtons = Ruleset != null,
- Defaults = defaultGroup.Select(d => d.KeyCombination)
+ Defaults = defaultGroup.Select(d => d.KeyCombination),
+ BindingUpdated = onBindingUpdated
});
}
@@ -58,6 +65,16 @@ namespace osu.Game.Overlays.Settings.Sections.Input
Action = () => Children.OfType().ForEach(k => k.RestoreDefaults())
});
}
+
+ private void onBindingUpdated(KeyBindingRow sender)
+ {
+ if (AutoAdvanceTarget)
+ {
+ var next = Children.SkipWhile(c => c != sender).Skip(1).FirstOrDefault();
+ if (next != null)
+ GetContainingInputManager().ChangeFocus(next);
+ }
+ }
}
public class ResetButton : DangerousSettingsButton
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 21a65862f7..f7c372a037 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -110,9 +110,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows || RuntimeInfo.OS == RuntimeInfo.Platform.Linux)
{
t.NewLine();
- var formattedSource = MessageFormatter.FormatText(localisation.GetLocalisedBindableString(TabletSettingsStrings.NoTabletDetectedDescription(RuntimeInfo.OS == RuntimeInfo.Platform.Windows
- ? @"https://opentabletdriver.net/Wiki/FAQ/Windows"
- : @"https://opentabletdriver.net/Wiki/FAQ/Linux")).Value);
+ var formattedSource = MessageFormatter.FormatText(localisation.GetLocalisedBindableString(TabletSettingsStrings.NoTabletDetectedDescription(
+ RuntimeInfo.OS == RuntimeInfo.Platform.Windows
+ ? @"https://opentabletdriver.net/Wiki/FAQ/Windows"
+ : @"https://opentabletdriver.net/Wiki/FAQ/Linux")).Value);
t.AddLinks(formattedSource.Text, formattedSource.Links);
}
}),
@@ -273,6 +274,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
sizeY.Default = sizeY.MaxValue = tab.Size.Y;
areaSize.Default = new Vector2(sizeX.Default, sizeY.Default);
+ areaOffset.Default = new Vector2(offsetX.Default, offsetY.Default);
}), true);
}
diff --git a/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs
index a0f069b3bb..a6f6c28463 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs
@@ -8,6 +8,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
public class VariantBindingsSubsection : KeyBindingsSubsection
{
+ protected override bool AutoAdvanceTarget => true;
+
protected override LocalisableString Header { get; }
public VariantBindingsSubsection(RulesetInfo ruleset, int variant)
diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs
index a8fe3d04be..4d75537f6b 100644
--- a/osu.Game/Overlays/Settings/Sections/InputSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs
@@ -3,8 +3,6 @@
#nullable disable
-using System.Collections.Generic;
-using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
@@ -22,8 +20,6 @@ namespace osu.Game.Overlays.Settings.Sections
public override LocalisableString Header => InputSettingsStrings.InputSectionHeader;
- public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "keybindings" });
-
public override Drawable CreateIcon() => new SpriteIcon
{
Icon = FontAwesome.Solid.Keyboard
diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs
index eae13fe5d0..158350b43d 100644
--- a/osu.Game/PerformFromMenuRunner.cs
+++ b/osu.Game/PerformFromMenuRunner.cs
@@ -89,6 +89,10 @@ namespace osu.Game
// check if we are already at a valid target screen.
if (validScreens.Any(t => t.IsAssignableFrom(type)))
{
+ if (!((Drawable)current).IsLoaded)
+ // wait until screen is loaded before invoking action.
+ return true;
+
finalAction(current);
Cancel();
return true;
diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs
index 3fb12041d1..4f802a22a1 100644
--- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Difficulty
// calculate total score
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = perfectPlay.Mods;
- perfectPlay.TotalScore = (long)scoreProcessor.ComputeScore(ScoringMode.Standardised, perfectPlay);
+ perfectPlay.TotalScore = scoreProcessor.ComputeScore(ScoringMode.Standardised, perfectPlay);
// compute rank achieved
// default to SS, then adjust the rank with mods
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index 7456ce06bd..899d149cbc 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Scoring
///
/// The current total score.
///
- public readonly BindableDouble TotalScore = new BindableDouble { MinValue = 0 };
+ public readonly BindableLong TotalScore = new BindableLong { MinValue = 0 };
///
/// The current accuracy.
@@ -267,7 +267,7 @@ namespace osu.Game.Rulesets.Scoring
private void updateScore()
{
- Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1;
+ Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? (double)currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1;
TotalScore.Value = ComputeScore(Mode.Value, currentScoringValues, maximumScoringValues);
}
@@ -298,7 +298,7 @@ namespace osu.Game.Rulesets.Scoring
/// The to compute the total score of.
/// The total score in the given .
[Pure]
- public double ComputeScore(ScoringMode mode, ScoreInfo scoreInfo)
+ public long ComputeScore(ScoringMode mode, ScoreInfo scoreInfo)
{
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
@@ -316,9 +316,9 @@ namespace osu.Game.Rulesets.Scoring
/// The maximum scoring values.
/// The total score computed from the given scoring values.
[Pure]
- public double ComputeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum)
+ public long ComputeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum)
{
- double accuracyRatio = maximum.BaseScore > 0 ? current.BaseScore / maximum.BaseScore : 1;
+ double accuracyRatio = maximum.BaseScore > 0 ? (double)current.BaseScore / maximum.BaseScore : 1;
double comboRatio = maximum.MaxCombo > 0 ? (double)current.MaxCombo / maximum.MaxCombo : 1;
return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects);
}
@@ -333,21 +333,23 @@ namespace osu.Game.Rulesets.Scoring
/// The total number of basic (non-tick and non-bonus) hitobjects in the beatmap.
/// The total score computed from the given scoring component ratios.
[Pure]
- public double ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, double bonusScore, int totalBasicHitObjects)
+ public long ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, long bonusScore, int totalBasicHitObjects)
{
+ double accuracyScore = accuracyPortion * accuracyRatio;
+ double comboScore = comboPortion * comboRatio;
+ double rawScore = (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier;
+
switch (mode)
{
default:
case ScoringMode.Standardised:
- double accuracyScore = accuracyPortion * accuracyRatio;
- double comboScore = comboPortion * comboRatio;
- return (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier;
+ return (long)Math.Round(rawScore);
case ScoringMode.Classic:
// This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring.
// The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes.
- double scaledStandardised = ComputeScore(ScoringMode.Standardised, accuracyRatio, comboRatio, bonusScore, totalBasicHitObjects) / max_score;
- return Math.Pow(scaledStandardised * Math.Max(1, totalBasicHitObjects), 2) * ClassicScoreMultiplier;
+ double scaledRawScore = rawScore / max_score;
+ return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, totalBasicHitObjects), 2) * ClassicScoreMultiplier);
}
}
@@ -417,7 +419,7 @@ namespace osu.Game.Rulesets.Scoring
score.MaximumStatistics[result] = maximumResultCounts.GetValueOrDefault(result);
// Populate total score after everything else.
- score.TotalScore = (long)Math.Round(ComputeScore(ScoringMode.Standardised, score));
+ score.TotalScore = ComputeScore(ScoringMode.Standardised, score);
}
///
diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
index ad52b4affc..c50f63c3b2 100644
--- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
+++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
@@ -115,7 +115,11 @@ namespace osu.Game.Rulesets.UI
public Sample Get(string name) => primary.Get(name) ?? fallback.Get(name);
- public Task GetAsync(string name, CancellationToken cancellationToken = default) => primary.GetAsync(name, cancellationToken) ?? fallback.GetAsync(name, cancellationToken);
+ public async Task GetAsync(string name, CancellationToken cancellationToken = default)
+ {
+ return await primary.GetAsync(name, cancellationToken).ConfigureAwait(false)
+ ?? await fallback.GetAsync(name, cancellationToken).ConfigureAwait(false);
+ }
public Stream GetStream(string name) => primary.GetStream(name) ?? fallback.GetStream(name);
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index 8342d3bcc1..b2944ad219 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -99,7 +99,7 @@ namespace osu.Game.Scoring
var scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = score.Mods;
- return (long)Math.Round(scoreProcessor.ComputeScore(mode, score));
+ return scoreProcessor.ComputeScore(mode, score);
}
///
diff --git a/osu.Game/Scoring/ScoringValues.cs b/osu.Game/Scoring/ScoringValues.cs
index 9bc4e6e12a..471067c9db 100644
--- a/osu.Game/Scoring/ScoringValues.cs
+++ b/osu.Game/Scoring/ScoringValues.cs
@@ -20,13 +20,13 @@ namespace osu.Game.Scoring
/// The sum of all "basic" scoring values. See: and .
///
[Key(0)]
- public double BaseScore;
+ public long BaseScore;
///
/// The sum of all "bonus" scoring values. See: and .
///
[Key(1)]
- public double BonusScore;
+ public long BonusScore;
///
/// The highest achieved combo.
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
index 974e240552..20ef128ee9 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
@@ -436,8 +436,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
base.OnDragEnd(e);
- OnDragHandled?.Invoke(null);
+ dragOperation?.Cancel();
+ dragOperation = null;
+
changeHandler?.EndChange();
+ OnDragHandled?.Invoke(null);
}
}
diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
index 3d18b00e75..d3c330c6d7 100644
--- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
+++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets;
@@ -19,7 +20,7 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline;
namespace osu.Game.Screens.Edit.Compose
{
- public class ComposeScreen : EditorScreenWithTimeline
+ public class ComposeScreen : EditorScreenWithTimeline, IGameplaySettings
{
[Resolved]
private GameHost host { get; set; }
@@ -27,6 +28,9 @@ namespace osu.Game.Screens.Edit.Compose
[Resolved]
private EditorClock clock { get; set; }
+ [Resolved]
+ private IGameplaySettings globalGameplaySettings { get; set; }
+
private Bindable clipboard { get; set; }
private HitObjectComposer composer;
@@ -157,5 +161,12 @@ namespace osu.Game.Screens.Edit.Compose
}
#endregion
+
+ // Combo colour normalisation should not be applied in the editor.
+ // Note this doesn't affect editor test mode.
+ IBindable IGameplaySettings.ComboColourNormalisationAmount => new Bindable();
+
+ // Arguable.
+ IBindable IGameplaySettings.PositionalHitsoundsLevel => globalGameplaySettings.PositionalHitsoundsLevel;
}
}
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index bb390dfbf3..4c44117581 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -58,8 +58,7 @@ namespace osu.Game.Screens.Edit
{
[Cached(typeof(IBeatSnapProvider))]
[Cached]
- public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider, ISamplePlaybackDisabler, IBeatSyncProvider,
- IGameplaySettings
+ public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider, ISamplePlaybackDisabler, IBeatSyncProvider
{
public override float BackgroundParallaxAmount => 0.1f;
@@ -99,9 +98,6 @@ namespace osu.Game.Screens.Edit
[Resolved(canBeNull: true)]
private INotificationOverlay notifications { get; set; }
- [Resolved]
- private IGameplaySettings globalGameplaySettings { get; set; }
-
public readonly Bindable Mode = new Bindable();
public IBindable SamplePlaybackDisabled => samplePlaybackDisabled;
@@ -1045,11 +1041,5 @@ namespace osu.Game.Screens.Edit
{
}
}
-
- // Combo colour normalisation should not be applied in the editor.
- IBindable IGameplaySettings.ComboColourNormalisationAmount => new Bindable();
-
- // Arguable.
- IBindable IGameplaySettings.PositionalHitsoundsLevel => globalGameplaySettings.PositionalHitsoundsLevel;
}
}
diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index 81d82130da..f83874e4a0 100644
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -270,7 +270,7 @@ namespace osu.Game.Screens.Edit
{
IsSeeking &= Transforms.Any();
- if (track.Value?.IsRunning != true)
+ if (!IsRunning)
{
// seeking in the editor can happen while the track isn't running.
// in this case we always want to expose ourselves as seeking (to avoid sample playback).
diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
index 94975b6b5e..ad09618279 100644
--- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
+++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
@@ -1,8 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
@@ -17,7 +16,7 @@ namespace osu.Game.Screens.Edit.GameplayTest
private readonly EditorState editorState;
[Resolved]
- private MusicController musicController { get; set; }
+ private MusicController musicController { get; set; } = null!;
public EditorPlayer(Editor editor)
: base(new PlayerConfiguration { ShowResults = false })
@@ -29,7 +28,12 @@ namespace osu.Game.Screens.Edit.GameplayTest
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
{
var masterGameplayClockContainer = new MasterGameplayClockContainer(beatmap, gameplayStart);
- masterGameplayClockContainer.Reset(editorState.Time);
+
+ // Only reset the time to the current point if the editor is later than the normal start time (and the first object).
+ // This allows more sane test playing from the start of the beatmap (ie. correctly adding lead-in time).
+ if (editorState.Time > gameplayStart && editorState.Time > DrawableRuleset.Objects.FirstOrDefault()?.StartTime)
+ masterGameplayClockContainer.Reset(editorState.Time);
+
return masterGameplayClockContainer;
}
@@ -70,7 +74,6 @@ namespace osu.Game.Screens.Edit.GameplayTest
{
musicController.Stop();
- editorState.Time = GameplayClockContainer.CurrentTime;
editor.RestoreState(editorState);
return base.OnExiting(e);
}
diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs
index afba00274c..ac22fdce71 100644
--- a/osu.Game/Screens/Loader.cs
+++ b/osu.Game/Screens/Loader.cs
@@ -125,13 +125,11 @@ namespace osu.Game.Screens
[BackgroundDependencyLoader]
private void load(ShaderManager manager)
{
- loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED));
- loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR));
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE));
+ loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR));
loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE));
- loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE_ROUNDED));
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE));
}
diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs
index 05a6d25303..4ec877b85a 100644
--- a/osu.Game/Screens/Menu/IntroTriangles.cs
+++ b/osu.Game/Screens/Menu/IntroTriangles.cs
@@ -224,8 +224,8 @@ namespace osu.Game.Screens.Menu
{
rulesetsScale.ScaleTo(0.8f, 1000);
rulesets.FadeIn().ScaleTo(1).TransformSpacingTo(new Vector2(200, 0));
- welcomeText.FadeOut();
- triangles.FadeOut();
+ welcomeText.FadeOut().Expire();
+ triangles.FadeOut().Expire();
}
using (BeginDelayedSequence(rulesets_2))
@@ -307,7 +307,7 @@ namespace osu.Game.Screens.Menu
}
[BackgroundDependencyLoader]
- private void load(TextureStore textures)
+ private void load(LargeTextureStore textures)
{
InternalChildren = new Drawable[]
{
diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs
index c7bda4d8f8..4a20d7cb2b 100644
--- a/osu.Game/Screens/Menu/LogoVisualisation.cs
+++ b/osu.Game/Screens/Menu/LogoVisualisation.cs
@@ -89,7 +89,7 @@ namespace osu.Game.Screens.Menu
private void load(IRenderer renderer, ShaderManager shaders)
{
texture = renderer.WhitePixel;
- shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
+ shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
}
private readonly float[] temporalAmplitudes = new float[ChannelAmplitudes.AMPLITUDES_SIZE];
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
index 1c4d02bb11..2444729118 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
@@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
- Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.ComputeScore(ScoringMode.Standardised, Score.ScoreInfo));
+ Score.ScoreInfo.TotalScore = ScoreProcessor.ComputeScore(ScoringMode.Standardised, Score.ScoreInfo);
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
index 48908fb9a0..47b67fba00 100644
--- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
+++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
@@ -171,12 +171,14 @@ namespace osu.Game.Screens.Play.HUD
for (int i = 0; i < Flow.Count; i++)
{
Flow.SetLayoutPosition(orderedByScore[i], i);
- orderedByScore[i].ScorePosition = i + 1;
+ orderedByScore[i].ScorePosition = CheckValidScorePosition(i + 1) ? i + 1 : null;
}
sorting.Validate();
}
+ protected virtual bool CheckValidScorePosition(int i) => true;
+
private class InputDisabledScrollContainer : OsuScrollContainer
{
public InputDisabledScrollContainer()
diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
index 2eec8253b3..0d03d8d090 100644
--- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
+++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Screens.Play.HUD
private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText;
- public BindableDouble TotalScore { get; } = new BindableDouble();
+ public BindableLong TotalScore { get; } = new BindableLong();
public BindableDouble Accuracy { get; } = new BindableDouble(1);
public BindableInt Combo { get; } = new BindableInt();
public BindableBool HasQuit { get; } = new BindableBool();
@@ -62,20 +62,22 @@ namespace osu.Game.Screens.Play.HUD
private int? scorePosition;
+ private bool scorePositionIsSet;
+
public int? ScorePosition
{
get => scorePosition;
set
{
- if (value == scorePosition)
+ // We always want to run once, as the incoming value may be null and require a visual update to "-".
+ if (value == scorePosition && scorePositionIsSet)
return;
scorePosition = value;
- if (scorePosition.HasValue)
- positionText.Text = $"#{scorePosition.Value.FormatRank()}";
+ positionText.Text = scorePosition.HasValue ? $"#{scorePosition.Value.FormatRank()}" : "-";
+ scorePositionIsSet = true;
- positionText.FadeTo(scorePosition.HasValue ? 1 : 0);
updateState();
}
}
diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
index e7b2ce1672..c9f1571dfe 100644
--- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
+++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Linq;
using osu.Framework.Allocation;
@@ -11,6 +9,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
@@ -23,10 +22,9 @@ using osuTK;
namespace osu.Game.Screens.Play.HUD.HitErrorMeters
{
+ [Cached]
public class BarHitErrorMeter : HitErrorMeter
{
- private const int judgement_line_width = 14;
-
[SettingSource("Judgement line thickness", "How thick the individual lines should be.")]
public BindableNumber JudgementLineThickness { get; } = new BindableNumber(4)
{
@@ -44,28 +42,33 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
[SettingSource("Label style", "How to show early/late extremities")]
public Bindable LabelStyle { get; } = new Bindable(LabelStyles.Icons);
- private SpriteIcon arrow;
- private UprightAspectMaintainingContainer labelEarly;
- private UprightAspectMaintainingContainer labelLate;
+ private const int judgement_line_width = 14;
- private Container colourBarsEarly;
- private Container colourBarsLate;
+ private const int max_concurrent_judgements = 50;
- private Container judgementsContainer;
+ private const int centre_marker_size = 8;
private double maxHitWindow;
private double floatingAverage;
- private Container colourBars;
- private Container arrowContainer;
- private (HitResult result, double length)[] hitWindows;
+ private readonly DrawablePool judgementLinePool = new DrawablePool(50);
- private const int max_concurrent_judgements = 50;
+ private SpriteIcon arrow = null!;
+ private UprightAspectMaintainingContainer labelEarly = null!;
+ private UprightAspectMaintainingContainer labelLate = null!;
- private Drawable[] centreMarkerDrawables;
+ private Container colourBarsEarly = null!;
+ private Container colourBarsLate = null!;
- private const int centre_marker_size = 8;
+ private Container judgementsContainer = null!;
+
+ private Container colourBars = null!;
+ private Container arrowContainer = null!;
+
+ private (HitResult result, double length)[] hitWindows = null!;
+
+ private Drawable[]? centreMarkerDrawables;
public BarHitErrorMeter()
{
@@ -88,6 +91,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
Margin = new MarginPadding(2),
Children = new Drawable[]
{
+ judgementLinePool,
colourBars = new Container
{
Name = "colour axis",
@@ -403,11 +407,12 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
}
}
- judgementsContainer.Add(new JudgementLine
+ judgementLinePool.Get(drawableJudgement =>
{
- JudgementLineThickness = { BindTarget = JudgementLineThickness },
- Y = getRelativeJudgementPosition(judgement.TimeOffset),
- Colour = GetColourForHitResult(judgement.Type),
+ drawableJudgement.Y = getRelativeJudgementPosition(judgement.TimeOffset);
+ drawableJudgement.Colour = GetColourForHitResult(judgement.Type);
+
+ judgementsContainer.Add(drawableJudgement);
});
arrow.MoveToY(
@@ -417,10 +422,13 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
private float getRelativeJudgementPosition(double value) => Math.Clamp((float)((value / maxHitWindow) + 1) / 2, 0, 1);
- internal class JudgementLine : CompositeDrawable
+ internal class JudgementLine : PoolableDrawable
{
public readonly BindableNumber JudgementLineThickness = new BindableFloat();
+ [Resolved]
+ private BarHitErrorMeter barHitErrorMeter { get; set; } = null!;
+
public JudgementLine()
{
RelativeSizeAxes = Axes.X;
@@ -439,16 +447,22 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
protected override void LoadComplete()
{
+ base.LoadComplete();
+
+ JudgementLineThickness.BindTo(barHitErrorMeter.JudgementLineThickness);
+ JudgementLineThickness.BindValueChanged(thickness => Height = thickness.NewValue, true);
+ }
+
+ protected override void PrepareForUse()
+ {
+ base.PrepareForUse();
+
const int judgement_fade_in_duration = 100;
const int judgement_fade_out_duration = 5000;
- base.LoadComplete();
-
Alpha = 0;
Width = 0;
- JudgementLineThickness.BindValueChanged(thickness => Height = thickness.NewValue, true);
-
this
.FadeTo(0.6f, judgement_fade_in_duration, Easing.OutQuint)
.ResizeWidthTo(1, judgement_fade_in_duration, Easing.OutQuint)
diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs
index dadec7c06b..86ba85168f 100644
--- a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs
+++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs
@@ -3,9 +3,11 @@
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration;
using osu.Game.Rulesets.Judgements;
@@ -15,6 +17,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD.HitErrorMeters
{
+ [Cached]
public class ColourHitErrorMeter : HitErrorMeter
{
private const int animation_duration = 200;
@@ -82,7 +85,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
{
base.LoadComplete();
- JudgementCount.BindValueChanged(count =>
+ JudgementCount.BindValueChanged(_ =>
{
removeExtraJudgements();
updateMetrics();
@@ -91,14 +94,17 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
JudgementSpacing.BindValueChanged(_ => updateMetrics(), true);
}
+ private readonly DrawablePool judgementLinePool = new DrawablePool(50);
+
public void Push(Color4 colour)
{
- Add(new HitErrorShape(colour, drawable_judgement_size)
+ judgementLinePool.Get(shape =>
{
- Shape = { BindTarget = JudgementShape },
- });
+ shape.Colour = colour;
+ Add(shape);
- removeExtraJudgements();
+ removeExtraJudgements();
+ });
}
private void removeExtraJudgements()
@@ -116,32 +122,32 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
}
}
- public class HitErrorShape : Container
+ public class HitErrorShape : PoolableDrawable
{
public bool IsRemoved { get; private set; }
public readonly Bindable Shape = new Bindable();
- private readonly Color4 colour;
+ [Resolved]
+ private ColourHitErrorMeter hitErrorMeter { get; set; } = null!;
private Container content = null!;
- public HitErrorShape(Color4 colour, int size)
+ public HitErrorShape()
{
- this.colour = colour;
- Size = new Vector2(size);
+ Size = new Vector2(drawable_judgement_size);
}
protected override void LoadComplete()
{
base.LoadComplete();
- Child = content = new Container
+ InternalChild = content = new Container
{
RelativeSizeAxes = Axes.Both,
- Colour = colour
};
+ Shape.BindTo(hitErrorMeter.JudgementShape);
Shape.BindValueChanged(shape =>
{
switch (shape.NewValue)
@@ -155,17 +161,32 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
break;
}
}, true);
+ }
- content.FadeInFromZero(animation_duration, Easing.OutQuint);
- content.MoveToY(-DrawSize.Y);
- content.MoveToY(0, animation_duration, Easing.OutQuint);
+ protected override void PrepareForUse()
+ {
+ base.PrepareForUse();
+
+ this.FadeInFromZero(animation_duration, Easing.OutQuint)
+ // On pool re-use, start flow animation from (0,0).
+ .MoveTo(Vector2.Zero);
+
+ content.MoveToY(-DrawSize.Y)
+ .MoveToY(0, animation_duration, Easing.OutQuint);
+ }
+
+ protected override void FreeAfterUse()
+ {
+ base.FreeAfterUse();
+ IsRemoved = false;
}
public void Remove()
{
IsRemoved = true;
- this.FadeOut(animation_duration, Easing.OutQuint).Expire();
+ this.FadeOut(animation_duration, Easing.OutQuint)
+ .Expire();
}
}
diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs
index aa06bb08a5..428390f90c 100644
--- a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs
+++ b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs
@@ -9,7 +9,7 @@ namespace osu.Game.Screens.Play.HUD
{
public interface ILeaderboardScore
{
- BindableDouble TotalScore { get; }
+ BindableLong TotalScore { get; }
BindableDouble Accuracy { get; }
BindableInt Combo { get; }
diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
index 4201b3f4c9..4ac92056ef 100644
--- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
+++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
@@ -184,7 +183,7 @@ namespace osu.Game.Screens.Play.HUD
continue;
if (TeamScores.TryGetValue(u.Team.Value, out var team))
- team.Value += (int)Math.Round(u.ScoreProcessor.TotalScore.Value);
+ team.Value += u.ScoreProcessor.TotalScore.Value;
}
}
diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs
index ab3cf2950c..0df3200adb 100644
--- a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs
+++ b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs
@@ -7,8 +7,10 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Configuration;
+using osu.Game.Online.API.Requests;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
+using osu.Game.Screens.Select;
using osu.Game.Users;
namespace osu.Game.Screens.Play.HUD
@@ -18,6 +20,9 @@ namespace osu.Game.Screens.Play.HUD
private const int duration = 100;
private readonly Bindable configVisibility = new Bindable();
+
+ private readonly Bindable scoreSource = new Bindable();
+
private readonly IUser trackingUser;
public readonly IBindableList Scores = new BindableList();
@@ -46,11 +51,13 @@ namespace osu.Game.Screens.Play.HUD
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility);
+ config.BindWith(OsuSetting.BeatmapDetailTab, scoreSource);
}
protected override void LoadComplete()
{
base.LoadComplete();
+
Scores.BindCollectionChanged((_, _) => Scheduler.AddOnce(showScores), true);
// Alpha will be updated via `updateVisibility` below.
@@ -93,6 +100,18 @@ namespace osu.Game.Screens.Play.HUD
local.DisplayOrder.Value = long.MaxValue;
}
+ protected override bool CheckValidScorePosition(int i)
+ {
+ // change displayed position to '-' when there are 50 already submitted scores and tracked score is last
+ if (scoreSource.Value != PlayBeatmapDetailArea.TabType.Local)
+ {
+ if (i == Flow.Count && Flow.Count > GetScoresRequest.MAX_SCORES_PER_REQUEST)
+ return false;
+ }
+
+ return base.CheckValidScorePosition(i);
+ }
+
private void updateVisibility() =>
this.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration);
}
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index 345bd5a134..fb296d7988 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -85,7 +85,7 @@ namespace osu.Game.Screens.Play
api.Queue(req);
// Generally a timeout would not happen here as APIAccess will timeout first.
- if (!tcs.Task.Wait(60000))
+ if (!tcs.Task.Wait(30000))
req.TriggerFailure(new InvalidOperationException("Token retrieval timed out (request never run)"));
return true;
diff --git a/osu.Game/Screens/Select/Footer.cs b/osu.Game/Screens/Select/Footer.cs
index 86fe76c0c6..f9fc2890b0 100644
--- a/osu.Game/Screens/Select/Footer.cs
+++ b/osu.Game/Screens/Select/Footer.cs
@@ -57,7 +57,18 @@ namespace osu.Game.Screens.Select
}
}
- private void updateModeLight() => modeLight.FadeColour(buttons.FirstOrDefault(b => b.IsHovered)?.SelectedColour ?? Color4.Transparent, TRANSITION_LENGTH, Easing.OutQuint);
+ private void updateModeLight()
+ {
+ var selectedButton = buttons.FirstOrDefault(b => b.Enabled.Value && b.IsHovered);
+
+ if (selectedButton != null)
+ {
+ modeLight.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
+ modeLight.FadeColour(selectedButton.SelectedColour, TRANSITION_LENGTH, Easing.OutQuint);
+ }
+ else
+ modeLight.FadeOut(TRANSITION_LENGTH, Easing.OutQuint);
+ }
public Footer()
{
@@ -78,6 +89,7 @@ namespace osu.Game.Screens.Select
RelativeSizeAxes = Axes.X,
Height = 3,
Position = new Vector2(0, -3),
+ Colour = Color4.Black,
},
new FillFlowContainer
{
diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs
index 3f8cf2e13a..230cdfc13e 100644
--- a/osu.Game/Screens/Select/FooterButton.cs
+++ b/osu.Game/Screens/Select/FooterButton.cs
@@ -120,10 +120,18 @@ namespace osu.Game.Screens.Select
};
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ Enabled.BindValueChanged(_ => updateDisplay(), true);
+ }
+
public Action Hovered;
public Action HoverLost;
public GlobalAction? Hotkey;
+ private bool mouseDown;
+
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
@@ -140,32 +148,38 @@ namespace osu.Game.Screens.Select
protected override bool OnHover(HoverEvent e)
{
Hovered?.Invoke();
- light.ScaleTo(new Vector2(1, 2), Footer.TRANSITION_LENGTH, Easing.OutQuint);
- light.FadeColour(SelectedColour, Footer.TRANSITION_LENGTH, Easing.OutQuint);
+ updateDisplay();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
HoverLost?.Invoke();
- light.ScaleTo(new Vector2(1, 1), Footer.TRANSITION_LENGTH, Easing.OutQuint);
- light.FadeColour(DeselectedColour, Footer.TRANSITION_LENGTH, Easing.OutQuint);
+ updateDisplay();
}
protected override bool OnMouseDown(MouseDownEvent e)
{
- box.FadeTo(0.3f, Footer.TRANSITION_LENGTH * 2, Easing.OutQuint);
+ if (!Enabled.Value)
+ return true;
+
+ mouseDown = true;
+ updateDisplay();
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
- box.FadeOut(Footer.TRANSITION_LENGTH, Easing.OutQuint);
+ mouseDown = false;
+ updateDisplay();
base.OnMouseUp(e);
}
protected override bool OnClick(ClickEvent e)
{
+ if (!Enabled.Value)
+ return true;
+
box.ClearTransforms();
box.Alpha = 1;
box.FadeOut(Footer.TRANSITION_LENGTH * 3, Easing.OutQuint);
@@ -184,5 +198,20 @@ namespace osu.Game.Screens.Select
}
public virtual void OnReleased(KeyBindingReleaseEvent e) { }
+
+ private void updateDisplay()
+ {
+ this.FadeTo(Enabled.Value ? 1 : 0.25f, Footer.TRANSITION_LENGTH, Easing.OutQuint);
+
+ light.ScaleTo(Enabled.Value && IsHovered ? new Vector2(1, 2) : new Vector2(1), Footer.TRANSITION_LENGTH, Easing.OutQuint);
+ light.FadeColour(Enabled.Value && IsHovered ? SelectedColour : DeselectedColour, Footer.TRANSITION_LENGTH, Easing.OutQuint);
+
+ box.FadeTo(Enabled.Value & mouseDown ? 0.3f : 0f, Footer.TRANSITION_LENGTH * 2, Easing.OutQuint);
+
+ if (Enabled.Value && IsHovered)
+ Hovered?.Invoke();
+ else
+ HoverLost?.Invoke();
+ }
}
}
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 1add51e725..5d5019567a 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -112,6 +112,8 @@ namespace osu.Game.Screens.Select
protected BeatmapDetailArea BeatmapDetails { get; private set; }
+ private FooterButtonOptions beatmapOptionsButton;
+
private readonly Bindable decoupledRuleset = new Bindable();
private double audioFeedbackLastPlaybackTime;
@@ -314,7 +316,7 @@ namespace osu.Game.Screens.Select
NextRandom = () => Carousel.SelectNextRandom(),
PreviousRandom = Carousel.SelectPreviousRandom
}, null),
- (new FooterButtonOptions(), BeatmapOptions)
+ (beatmapOptionsButton = new FooterButtonOptions(), BeatmapOptions)
};
protected virtual ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay();
@@ -739,6 +741,16 @@ namespace osu.Game.Screens.Select
beatmapInfoWedge.Beatmap = beatmap;
BeatmapDetails.Beatmap = beatmap;
+
+ bool beatmapSelected = beatmap is not DummyWorkingBeatmap;
+
+ if (beatmapSelected)
+ beatmapOptionsButton.Enabled.Value = true;
+ else
+ {
+ beatmapOptionsButton.Enabled.Value = false;
+ BeatmapOptions.Hide();
+ }
}
private readonly WeakReference lastTrack = new WeakReference(null);
diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs
index a2eb07eba3..6a0c4a23e5 100644
--- a/osu.Game/Skinning/ArgonSkin.cs
+++ b/osu.Game/Skinning/ArgonSkin.cs
@@ -82,21 +82,14 @@ namespace osu.Game.Skinning
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{
+ // Temporary until default skin has a valid hit lighting.
+ if ((lookup as SkinnableSprite.SpriteComponentLookup)?.LookupName == @"lighting") return Drawable.Empty();
+
if (base.GetDrawableComponent(lookup) is Drawable c)
return c;
switch (lookup)
{
- case SkinnableSprite.SpriteComponentLookup spriteLookup:
- switch (spriteLookup.LookupName)
- {
- // Temporary until default skin has a valid hit lighting.
- case @"lighting":
- return Drawable.Empty();
- }
-
- break;
-
case GlobalSkinComponentLookup globalLookup:
switch (globalLookup.Lookup)
{
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 98618e3dcd..ea223d172d 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -396,9 +396,6 @@ namespace osu.Game.Skinning
}
return null;
-
- case SkinnableSprite.SpriteComponentLookup sprite:
- return this.GetAnimation(sprite.LookupName, false, false);
}
return null;
diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs
index e222b9017c..25d1dc903c 100644
--- a/osu.Game/Skinning/Skin.cs
+++ b/osu.Game/Skinning/Skin.cs
@@ -158,6 +158,10 @@ namespace osu.Game.Skinning
{
switch (lookup)
{
+ // This fallback is important for user skins which use SkinnableSprites.
+ case SkinnableSprite.SpriteComponentLookup sprite:
+ return this.GetAnimation(sprite.LookupName, false, false);
+
case GlobalSkinComponentLookup target:
if (!DrawableComponentInfo.TryGetValue(target.Lookup, out var skinnableInfo))
return null;
diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs
index 2075cfb6f2..62ef94691b 100644
--- a/osu.Game/Skinning/TrianglesSkin.cs
+++ b/osu.Game/Skinning/TrianglesSkin.cs
@@ -60,21 +60,14 @@ namespace osu.Game.Skinning
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{
+ // Temporary until default skin has a valid hit lighting.
+ if ((lookup as SkinnableSprite.SpriteComponentLookup)?.LookupName == @"lighting") return Drawable.Empty();
+
if (base.GetDrawableComponent(lookup) is Drawable c)
return c;
switch (lookup)
{
- case SkinnableSprite.SpriteComponentLookup spriteLookup:
- switch (spriteLookup.LookupName)
- {
- // Temporary until default skin has a valid hit lighting.
- case @"lighting":
- return Drawable.Empty();
- }
-
- break;
-
case GlobalSkinComponentLookup target:
switch (target.Lookup)
{
diff --git a/osu.Game/Tests/PollingNotificationsClient.cs b/osu.Game/Tests/PollingNotificationsClient.cs
new file mode 100644
index 0000000000..c1f032a647
--- /dev/null
+++ b/osu.Game/Tests/PollingNotificationsClient.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Threading;
+using System.Threading.Tasks;
+using osu.Game.Online.API;
+using osu.Game.Online.Notifications;
+
+namespace osu.Game.Tests
+{
+ ///
+ /// A notifications client which polls for new messages every second.
+ ///
+ public class PollingNotificationsClient : NotificationsClient
+ {
+ public PollingNotificationsClient(IAPIProvider api)
+ : base(api)
+ {
+ }
+
+ public override Task ConnectAsync(CancellationToken cancellationToken)
+ {
+ Task.Run(async () =>
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ await API.PerformAsync(CreateFetchMessagesRequest());
+ await Task.Delay(1000, cancellationToken);
+ }
+ }, cancellationToken);
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/osu.Game/Tests/PollingNotificationsClientConnector.cs b/osu.Game/Tests/PollingNotificationsClientConnector.cs
new file mode 100644
index 0000000000..823fc9d157
--- /dev/null
+++ b/osu.Game/Tests/PollingNotificationsClientConnector.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Threading;
+using System.Threading.Tasks;
+using osu.Game.Online.API;
+using osu.Game.Online.Notifications;
+
+namespace osu.Game.Tests
+{
+ ///
+ /// A connector for s that poll for new messages.
+ ///
+ public class PollingNotificationsClientConnector : NotificationsClientConnector
+ {
+ public PollingNotificationsClientConnector(IAPIProvider api)
+ : base(api)
+ {
+ }
+
+ protected override Task BuildNotificationClientAsync(CancellationToken cancellationToken)
+ => Task.FromResult((NotificationsClient)new PollingNotificationsClient(API));
+ }
+}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 09b1bb7162..858cac1dac 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -35,8 +35,8 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 4264d9220e..25217b872b 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,8 +61,8 @@
-
-
+
+
@@ -82,7 +82,7 @@
-
+