mirror of
https://github.com/ppy/osu.git
synced 2025-02-14 01:42:59 +08:00
Merge branch 'master' into fix-tablet-aspect-ratio-values
This commit is contained in:
commit
0c671a2a82
12
UseLocalResources.ps1
Normal file
12
UseLocalResources.ps1
Normal file
@ -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
|
11
UseLocalResources.sh
Executable file
11
UseLocalResources.sh
Executable file
@ -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
|
@ -51,8 +51,8 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1103.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1110.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1113.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1113.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
|
@ -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;
|
||||
});
|
||||
|
||||
|
@ -248,6 +248,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
break;
|
||||
}
|
||||
|
||||
slider.Path.ExpectedDistance.Value = null;
|
||||
piece.ControlPoint.Type = type;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -0,0 +1,39 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<TaikoPlayfield>().ForEach(p => p.Height = Math.Max(0.2f, (p.Height + 0.2f) % 1f)), 50);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKiai()
|
||||
{
|
||||
bool kiai = false;
|
||||
|
||||
AddStep("Toggle kiai", () =>
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
83
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonBarLine.cs
Normal file
83
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonBarLine.cs
Normal file
@ -0,0 +1,83 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.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<bool> 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<bool> major)
|
||||
{
|
||||
line.Alpha = major.NewValue ? 1f : 0.5f;
|
||||
line.Width = major.NewValue ? 1 : 0.5f;
|
||||
majorEdgeContainer.Alpha = major.NewValue ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.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)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
116
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs
Normal file
116
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs
Normal file
@ -0,0 +1,116 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.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;
|
||||
|
||||
/// <summary>
|
||||
/// The colour of the inner circle and outer glows.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.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;
|
||||
}
|
||||
}
|
||||
}
|
87
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitExplosion.cs
Normal file
87
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitExplosion.cs
Normal file
@ -0,0 +1,87 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using 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);
|
||||
}
|
||||
}
|
||||
}
|
72
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitTarget.cs
Normal file
72
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitTarget.cs
Normal file
@ -0,0 +1,72 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.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
|
||||
{
|
||||
/// <summary>
|
||||
/// Thickness of all drawn line pieces.
|
||||
/// </summary>
|
||||
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)),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
218
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonInputDrum.cs
Normal file
218
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonInputDrum.cs
Normal file
@ -0,0 +1,218 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A half-drum. Contains one centre and one rim hit.
|
||||
/// </summary>
|
||||
private class TaikoHalfDrum : CompositeDrawable, IKeyBindingHandler<TaikoAction>
|
||||
{
|
||||
/// <summary>
|
||||
/// The key to be used for the rim of the half-drum.
|
||||
/// </summary>
|
||||
public TaikoAction RimAction;
|
||||
|
||||
/// <summary>
|
||||
/// The key to be used for the centre of the half-drum.
|
||||
/// </summary>
|
||||
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<TaikoAction> 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<TaikoAction> e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
198
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs
Normal file
198
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs
Normal file
@ -0,0 +1,198 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays the default animation for this judgement piece.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The base implementation only handles fade (for all result types) and misses.
|
||||
/// Individual rulesets are recommended to implement their appropriate hit animations.
|
||||
/// </remarks>
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.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)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.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)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
68
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonTickPiece.cs
Normal file
68
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonTickPiece.cs
Normal file
@ -0,0 +1,68 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.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<bool> isFirstTick = new Bindable<bool>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
40
osu.Game.Rulesets.Taiko/Skinning/Argon/RingPiece.cs
Normal file
40
osu.Game.Rulesets.Taiko/Skinning/Argon/RingPiece.cs
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.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<HitResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ namespace osu.Game.Tests.Chat
|
||||
private ChannelManager channelManager;
|
||||
private int currentMessageId;
|
||||
private List<Message> sentMessages;
|
||||
private List<int> silencedUserIds;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
@ -39,6 +40,7 @@ namespace osu.Game.Tests.Chat
|
||||
{
|
||||
currentMessageId = 0;
|
||||
sentMessages = new List<Message>();
|
||||
silencedUserIds = new List<int>();
|
||||
|
||||
((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<Channel>()
|
||||
});
|
||||
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);
|
||||
|
@ -65,7 +65,7 @@ namespace osu.Game.Tests.Database
|
||||
private class TestLegacyBeatmapImporter : LegacyBeatmapImporter
|
||||
{
|
||||
public TestLegacyBeatmapImporter()
|
||||
: base(null)
|
||||
: base(null!)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -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]
|
||||
|
@ -0,0 +1,40 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<GameplayClockContainer>().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()
|
||||
|
@ -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);
|
||||
|
@ -163,10 +163,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
AddUntilStep("wait for bars to disappear", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
|
||||
AddUntilStep("ensure max circles not exceeded", () =>
|
||||
{
|
||||
return this.ChildrenOfType<ColourHitErrorMeter>()
|
||||
.All(m => m.ChildrenOfType<ColourHitErrorMeter.HitErrorShape>().Count() <= max_displayed_judgements);
|
||||
});
|
||||
this.ChildrenOfType<ColourHitErrorMeter>().First().ChildrenOfType<ColourHitErrorMeter.HitErrorShape>().Count(), () => Is.LessThanOrEqualTo(max_displayed_judgements));
|
||||
|
||||
AddStep("show displays", () =>
|
||||
{
|
||||
|
@ -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<ScoreInfo> scores = new BindableList<ScoreInfo>();
|
||||
|
||||
private readonly Bindable<bool> configVisibility = new Bindable<bool>();
|
||||
private readonly Bindable<PlayBeatmapDetailArea.TabType> beatmapTabType = new Bindable<PlayBeatmapDetailArea.TabType>();
|
||||
|
||||
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<GameplayLeaderboardScore>().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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<KeyCounter>().First();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<BeatmapSetInfo> getImport)
|
||||
{
|
||||
AddUntilStep("beatmap in song select", () =>
|
||||
{
|
||||
var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen;
|
||||
return songSelect.ChildrenOfType<BeatmapCarousel>().Single().BeatmapSets.Any(b => b.MatchesOnlineID(getImport()));
|
||||
});
|
||||
}
|
||||
|
||||
private void presentAndConfirm(Func<BeatmapSetInfo> 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<BeatmapSetInfo> getImport, int importedID)
|
||||
@ -148,9 +161,9 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
Predicate<BeatmapInfo> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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<Message> createChannelMessages(Channel channel)
|
||||
{
|
||||
var message = new Message
|
||||
var message = new Message(currentMessageId++)
|
||||
{
|
||||
ChannelId = channel.Id,
|
||||
Content = $"Hello, this is a message in {channel.Name}",
|
||||
|
@ -56,7 +56,9 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
Add(channelManager = new ChannelManager(parent.Get<IAPIProvider>()));
|
||||
var api = parent.Get<IAPIProvider>();
|
||||
|
||||
Add(channelManager = new ChannelManager(api));
|
||||
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
|
@ -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<RestoreDefaultValueButton<bool>>().First().Alpha == 0);
|
||||
|
||||
AddAssert("binding cleared", () => settingsKeyBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0)));
|
||||
AddAssert("binding cleared",
|
||||
() => settingsKeyBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().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<RestoreDefaultValueButton<bool>>().First().Alpha == 0);
|
||||
|
||||
AddAssert("binding cleared", () => settingsKeyBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0)));
|
||||
AddAssert("binding cleared",
|
||||
() => settingsKeyBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -256,8 +295,8 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
var firstRow = panel.ChildrenOfType<KeyBindingRow>().First(r => r.ChildrenOfType<OsuSpriteText>().Any(s => s.Text.ToString() == name));
|
||||
var firstButton = firstRow.ChildrenOfType<KeyBindingRow.KeyButton>().First();
|
||||
|
||||
return firstButton.Text.Text == keyName;
|
||||
});
|
||||
return firstButton.Text.Text.ToString();
|
||||
}, () => Is.EqualTo(keyName));
|
||||
}
|
||||
|
||||
private void scrollToAndStartBinding(string name)
|
||||
|
@ -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<FooterButtonOptions>().Single().Enabled.Value);
|
||||
AddStep("delete all beatmaps", () => manager.Delete());
|
||||
AddAssert("options disabled", () => !songSelect.ChildrenOfType<FooterButtonOptions>().Single().Enabled.Value);
|
||||
}
|
||||
|
||||
private void waitForInitialSelection()
|
||||
{
|
||||
AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault);
|
||||
|
@ -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<FooterButton>().Last().Enabled.Toggle(), 20);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFooterRandom()
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -8,7 +8,7 @@ namespace osu.Game.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// A settings provider which generally sources from <see cref="OsuConfigManager"/> (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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More settings can be moved into this interface as required.
|
||||
|
@ -1,11 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable 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<string>();
|
||||
|
||||
List<string> paths = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (string directory in storage.GetDirectories(string.Empty))
|
||||
{
|
||||
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))
|
||||
yield return subDirectory;
|
||||
paths.Add(subDirectory);
|
||||
}
|
||||
else
|
||||
yield return storage.GetFullPath(directory);
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
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<BeatmapSetInfo> importer)
|
||||
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// sqrt(3) / 2
|
||||
/// </summary>
|
||||
private const float equilateral_triangle_ratio = 0.866f;
|
||||
|
||||
/// <summary>
|
||||
/// How many screen-space pixels are smoothed over.
|
||||
/// Same behavior as Sprite's EdgeSmoothness.
|
||||
@ -69,7 +75,13 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a triangle particle with a random scale.
|
||||
/// </summary>
|
||||
@ -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),
|
||||
|
@ -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<float>("progress").UpdateValue(ref progress);
|
||||
TextureShader.GetUniform<float>("progress").UpdateValue(ref progress);
|
||||
|
||||
base.Blit(renderer);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ using osu.Game.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public abstract class ScoreCounter : RollingCounter<double>
|
||||
public abstract class ScoreCounter : RollingCounter<long>
|
||||
{
|
||||
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));
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
/// <param name="preferMessagePack">Whether to use MessagePack for serialisation if available on this platform.</param>
|
||||
IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack = true);
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new <see cref="NotificationsClientConnector"/>.
|
||||
/// </summary>
|
||||
NotificationsClientConnector GetNotificationsConnector();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new user account. This is a blocking operation.
|
||||
/// </summary>
|
||||
|
39
osu.Game/Online/API/Requests/ChatAckRequest.cs
Normal file
39
osu.Game/Online/API/Requests/ChatAckRequest.cs
Normal file
@ -0,0 +1,39 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Net.Http;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see cref="SinceMessageId"/> or <see cref="SinceSilenceId"/> is not null).
|
||||
///
|
||||
/// For silence handling, a <see cref="SinceMessageId"/> should be provided as soon as a message is received by the client.
|
||||
/// From that point forward, <see cref="SinceSilenceId"/> should be preferred after the first <see cref="ChatSilence"/>
|
||||
/// arrives in a response from the ack request. Specifying both parameters will prioritise the latter.
|
||||
/// </summary>
|
||||
public class ChatAckRequest : APIRequest<ChatAckResponse>
|
||||
{
|
||||
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";
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
19
osu.Game/Online/API/Requests/GetChannelRequest.cs
Normal file
19
osu.Game/Online/API/Requests/GetChannelRequest.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class GetChannelRequest : APIRequest<GetChannelResponse>
|
||||
{
|
||||
private readonly long channelId;
|
||||
|
||||
public GetChannelRequest(long channelId)
|
||||
{
|
||||
this.channelId = channelId;
|
||||
}
|
||||
|
||||
protected override string Target => $"chat/channels/{channelId}";
|
||||
}
|
||||
}
|
12
osu.Game/Online/API/Requests/GetNotificationsRequest.cs
Normal file
12
osu.Game/Online/API/Requests/GetNotificationsRequest.cs
Normal file
@ -0,0 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class GetNotificationsRequest : APIRequest<APINotificationsBundle>
|
||||
{
|
||||
protected override string Target => @"notifications";
|
||||
}
|
||||
}
|
@ -16,6 +16,8 @@ namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class GetScoresRequest : APIRequest<APIScoresCollection>
|
||||
{
|
||||
public const int MAX_SCORES_PER_REQUEST = 50;
|
||||
|
||||
private readonly IBeatmapInfo beatmapInfo;
|
||||
private readonly BeatmapLeaderboardScope scope;
|
||||
private readonly IRulesetInfo ruleset;
|
||||
|
@ -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;
|
||||
}
|
||||
|
37
osu.Game/Online/API/Requests/Responses/APINotification.cs
Normal file
37
osu.Game/Online/API/Requests/Responses/APINotification.cs
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using 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; }
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using 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!;
|
||||
}
|
||||
}
|
15
osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs
Normal file
15
osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs
Normal file
@ -0,0 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.API.Requests.Responses
|
||||
{
|
||||
[JsonObject(MemberSerialization.OptIn)]
|
||||
public class ChatAckResponse
|
||||
{
|
||||
[JsonProperty("silences")]
|
||||
public List<ChatSilence> Silences { get; set; } = null!;
|
||||
}
|
||||
}
|
17
osu.Game/Online/API/Requests/Responses/ChatSilence.cs
Normal file
17
osu.Game/Online/API/Requests/Responses/ChatSilence.cs
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using 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; }
|
||||
}
|
||||
}
|
19
osu.Game/Online/API/Requests/Responses/GetChannelResponse.cs
Normal file
19
osu.Game/Online/API/Requests/Responses/GetChannelResponse.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.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<APIUser> Users { get; set; } = null!;
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -134,6 +134,14 @@ namespace osu.Game.Online.Chat
|
||||
/// <param name="messages"></param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace or remove a message from the channel.
|
||||
/// </summary>
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
/// <summary>
|
||||
/// Manages everything channel related
|
||||
/// </summary>
|
||||
public class ChannelManager : PollingComponent, IChannelPostTarget
|
||||
public class ChannelManager : CompositeComponent, IChannelPostTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// The channels the player joins on startup
|
||||
@ -64,44 +65,50 @@ namespace osu.Game.Online.Chat
|
||||
public IBindableList<Channel> 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> apiState = new Bindable<APIState>();
|
||||
private bool channelsInitialised;
|
||||
private ScheduledDelegate scheduledAck;
|
||||
|
||||
private readonly IBindable<bool> 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)));
|
||||
|
||||
connector.NewMessages += msgs => Schedule(() => addMessages(msgs));
|
||||
|
||||
connector.PresenceReceived += () => Schedule(() =>
|
||||
{
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
private void updatePollRate(ValueChangedEvent<bool> 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.Start();
|
||||
|
||||
if (isIdle.Value)
|
||||
millisecondsBetweenPolls *= 10;
|
||||
|
||||
if (TimeBetweenPolls.Value != millisecondsBetweenPolls)
|
||||
{
|
||||
TimeBetweenPolls.Value = millisecondsBetweenPolls;
|
||||
Logger.Log($"Chat is now polling every {TimeBetweenPolls.Value} ms");
|
||||
}
|
||||
apiState.BindTo(api.State);
|
||||
apiState.BindValueChanged(_ => SendAck(), true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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<Message> messages)
|
||||
private void addMessages(List<Message> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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<bool>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the <paramref name="channel"/> as read
|
||||
/// </summary>
|
||||
@ -646,6 +644,12 @@ namespace osu.Game.Online.Chat
|
||||
|
||||
api.Queue(req);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
connector?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -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 };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A unique identifier for this message. Sent to and from osu!web to use for deduplication.
|
||||
/// </summary>
|
||||
[JsonProperty(@"uuid")]
|
||||
public string Uuid { get; set; } = string.Empty;
|
||||
|
||||
[JsonConstructor]
|
||||
public Message()
|
||||
{
|
||||
|
@ -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<PersistentEndpointClient> BuildConnectionAsync(CancellationToken cancellationToken)
|
||||
|
@ -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,6 +728,8 @@ namespace osu.Game.Online.Multiplayer
|
||||
if (Room == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Debug.Assert(APIRoom != null);
|
||||
|
||||
Room.Playlist[Room.Playlist.IndexOf(Room.Playlist.Single(existing => existing.ID == item.ID))] = item;
|
||||
@ -734,6 +737,11 @@ namespace osu.Game.Online.Multiplayer
|
||||
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();
|
||||
|
76
osu.Game/Online/Notifications/NotificationsClient.cs
Normal file
76
osu.Game/Online/Notifications/NotificationsClient.cs
Normal file
@ -0,0 +1,76 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.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
|
||||
{
|
||||
/// <summary>
|
||||
/// An abstract client which receives notification-related events (chat/notifications).
|
||||
/// </summary>
|
||||
public abstract class NotificationsClient : PersistentEndpointClient
|
||||
{
|
||||
public Action<Channel>? ChannelJoined;
|
||||
public Action<Channel>? ChannelParted;
|
||||
public Action<List<Message>>? 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<Message> messages)
|
||||
{
|
||||
NewMessages?.Invoke(messages);
|
||||
lastMessageId = Math.Max(lastMessageId, messages.LastOrDefault()?.Id ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Chat;
|
||||
|
||||
namespace osu.Game.Online.Notifications
|
||||
{
|
||||
/// <summary>
|
||||
/// An abstract connector or <see cref="NotificationsClient"/>s.
|
||||
/// </summary>
|
||||
public abstract class NotificationsClientConnector : PersistentEndpointClientConnector
|
||||
{
|
||||
public event Action<Channel>? ChannelJoined;
|
||||
public event Action<Channel>? ChannelParted;
|
||||
public event Action<List<Message>>? NewMessages;
|
||||
public event Action? PresenceReceived;
|
||||
|
||||
protected NotificationsClientConnector(IAPIProvider api)
|
||||
: base(api)
|
||||
{
|
||||
}
|
||||
|
||||
protected sealed override async Task<PersistentEndpointClient> 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<NotificationsClient> BuildNotificationClientAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
19
osu.Game/Online/Notifications/WebSocket/EndChatRequest.cs
Normal file
19
osu.Game/Online/Notifications/WebSocket/EndChatRequest.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// A websocket message notifying the server that the client no longer wants to receive chat messages.
|
||||
/// </summary>
|
||||
[JsonObject(MemberSerialization.OptIn)]
|
||||
public class EndChatRequest : SocketMessage
|
||||
{
|
||||
public EndChatRequest()
|
||||
{
|
||||
Event = @"chat.end";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.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
|
||||
{
|
||||
/// <summary>
|
||||
/// A websocket message sent from the server when new messages arrive.
|
||||
/// </summary>
|
||||
[JsonObject(MemberSerialization.OptIn)]
|
||||
public class NewChatMessageData
|
||||
{
|
||||
[JsonProperty(@"messages")]
|
||||
public List<Message> Messages { get; set; } = null!;
|
||||
|
||||
[JsonProperty(@"users")]
|
||||
private List<APIUser> users { get; set; } = null!;
|
||||
|
||||
[OnDeserialized]
|
||||
private void onDeserialised(StreamingContext context)
|
||||
{
|
||||
foreach (var m in Messages)
|
||||
m.Sender = users.Single(u => u.OnlineID == m.SenderId);
|
||||
}
|
||||
}
|
||||
}
|
24
osu.Game/Online/Notifications/WebSocket/SocketMessage.cs
Normal file
24
osu.Game/Online/Notifications/WebSocket/SocketMessage.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// A websocket message, sent either from the client or server.
|
||||
/// </summary>
|
||||
[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; }
|
||||
}
|
||||
}
|
19
osu.Game/Online/Notifications/WebSocket/StartChatRequest.cs
Normal file
19
osu.Game/Online/Notifications/WebSocket/StartChatRequest.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// A websocket message notifying the server that the client wants to receive chat messages.
|
||||
/// </summary>
|
||||
[JsonObject(MemberSerialization.OptIn)]
|
||||
public class StartChatRequest : SocketMessage
|
||||
{
|
||||
public StartChatRequest()
|
||||
{
|
||||
Event = @"chat.start";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,179 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.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
|
||||
{
|
||||
/// <summary>
|
||||
/// A notifications client which receives events via a websocket.
|
||||
/// </summary>
|
||||
public class WebSocketNotificationsClient : NotificationsClient
|
||||
{
|
||||
private readonly ClientWebSocket socket;
|
||||
private readonly string endpoint;
|
||||
private readonly ConcurrentDictionary<long, Channel> channelsMap = new ConcurrentDictionary<long, Channel>();
|
||||
|
||||
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<SocketMessage>(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<Channel>(message.Data.ToString());
|
||||
Debug.Assert(joinedChannel != null);
|
||||
|
||||
HandleChannelJoined(joinedChannel);
|
||||
break;
|
||||
|
||||
case @"chat.channel.part":
|
||||
Debug.Assert(message.Data != null);
|
||||
|
||||
Channel? partedChannel = JsonConvert.DeserializeObject<Channel>(message.Data.ToString());
|
||||
Debug.Assert(partedChannel != null);
|
||||
|
||||
HandleChannelParted(partedChannel);
|
||||
break;
|
||||
|
||||
case @"chat.message.new":
|
||||
Debug.Assert(message.Data != null);
|
||||
|
||||
NewChatMessageData? messageData = JsonConvert.DeserializeObject<NewChatMessageData>(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<Channel> getChannel(long channelId)
|
||||
{
|
||||
if (channelsMap.TryGetValue(channelId, out Channel channel))
|
||||
return channel;
|
||||
|
||||
var tsc = new TaskCompletionSource<Channel>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.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
|
||||
{
|
||||
/// <summary>
|
||||
/// A connector for <see cref="WebSocketNotificationsClient"/>s that receive events via a websocket.
|
||||
/// </summary>
|
||||
public class WebSocketNotificationsClientConnector : NotificationsClientConnector
|
||||
{
|
||||
private readonly IAPIProvider api;
|
||||
|
||||
public WebSocketNotificationsClientConnector(IAPIProvider api)
|
||||
: base(api)
|
||||
{
|
||||
this.api = api;
|
||||
}
|
||||
|
||||
protected override async Task<NotificationsClient> BuildNotificationClientAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,11 +23,13 @@ namespace osu.Game.Online
|
||||
/// </summary>
|
||||
public PersistentEndpointClient? CurrentConnection { get; private set; }
|
||||
|
||||
protected readonly IAPIProvider API;
|
||||
|
||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||
private readonly Bindable<bool> isConnected = new Bindable<bool>();
|
||||
private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1);
|
||||
private CancellationTokenSource connectCancelSource = new CancellationTokenSource();
|
||||
|
||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||
private bool started;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new <see cref="PersistentEndpointClientConnector"/>.
|
||||
@ -35,8 +37,20 @@ namespace osu.Game.Online
|
||||
/// <param name="api"> An API provider used to react to connection state changes.</param>
|
||||
protected PersistentEndpointClientConnector(IAPIProvider api)
|
||||
{
|
||||
API = api;
|
||||
apiState.BindTo(api.State);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to connect and begins processing messages from the remote endpoint.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Online.Rooms
|
||||
/// The total scores in the playlist item.
|
||||
/// </summary>
|
||||
[JsonProperty("total")]
|
||||
public int? TotalScores { get; set; }
|
||||
public long? TotalScores { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's score, if any.
|
||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Online.Spectator
|
||||
/// <summary>
|
||||
/// The current total score.
|
||||
/// </summary>
|
||||
public readonly BindableDouble TotalScore = new BindableDouble { MinValue = 0 };
|
||||
public readonly BindableLong TotalScore = new BindableLong { MinValue = 0 };
|
||||
|
||||
/// <summary>
|
||||
/// The current accuracy.
|
||||
|
@ -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());
|
||||
|
@ -41,11 +41,8 @@ namespace osu.Game.Overlays
|
||||
|
||||
private IBindable<APIUser> apiUser;
|
||||
|
||||
private Drawable currentContent;
|
||||
private Container panelTarget;
|
||||
private FillFlowContainer<BeatmapCard> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether <paramref name="drawable"/> is a static placeholder reused multiple times by this overlay.
|
||||
/// </summary>
|
||||
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<LocalisableString> filtersUsed;
|
||||
|
||||
public SupporterRequiredDrawable(List<LocalisableString> 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<LocalisableString> 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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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));
|
||||
|
@ -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))
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "keybindings" });
|
||||
|
||||
public BindingSettings(KeyBindingPanel keyConfig)
|
||||
{
|
||||
Children = new Drawable[]
|
||||
|
@ -33,6 +33,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
{
|
||||
public class KeyBindingRow : Container, IFilterable
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the binding of this row is updated with a change being written.
|
||||
/// </summary>
|
||||
public Action<KeyBindingRow> BindingUpdated { get; set; }
|
||||
|
||||
private readonly object action;
|
||||
private readonly IEnumerable<RealmKeyBinding> 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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update from a key combination, only allowing a single non-modifier key to be specified.
|
||||
/// </summary>
|
||||
/// <param name="fullState">A <see cref="KeyCombination"/> generated from the full input state.</param>
|
||||
/// <param name="triggerKey">The key which triggered this update, and should be used as the binding.</param>
|
||||
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))
|
||||
|
@ -19,6 +19,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
{
|
||||
public abstract class KeyBindingsSubsection : SettingsSubsection
|
||||
{
|
||||
/// <summary>
|
||||
/// After a successful binding, automatically select the next binding row to make quickly
|
||||
/// binding a large set of keys easier on the user.
|
||||
/// </summary>
|
||||
protected virtual bool AutoAdvanceTarget => false;
|
||||
|
||||
protected IEnumerable<Framework.Input.Bindings.KeyBinding> 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<KeyBindingRow>().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
|
||||
|
@ -110,7 +110,8 @@ 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
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user