mirror of
https://github.com/ppy/osu.git
synced 2024-11-13 16:47:46 +08:00
Merge branch 'master' into results-clean
This commit is contained in:
commit
7b2adc857a
@ -3,13 +3,13 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"jetbrains.resharper.globaltools": {
|
||||
"version": "2022.2.3",
|
||||
"version": "2023.3.3",
|
||||
"commands": [
|
||||
"jb"
|
||||
]
|
||||
},
|
||||
"nvika": {
|
||||
"version": "2.2.0",
|
||||
"version": "3.0.0",
|
||||
"commands": [
|
||||
"nvika"
|
||||
]
|
||||
|
@ -1,5 +1,3 @@
|
||||
is_global = true
|
||||
|
||||
# .NET Code Style
|
||||
# IDE styles reference: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/
|
||||
|
||||
@ -56,4 +54,4 @@ dotnet_diagnostic.RS0030.severity = error
|
||||
|
||||
# Temporarily disable analysing CanBeNull = true in NRT contexts due to mobile issues.
|
||||
# See: https://github.com/ppy/osu/pull/19677
|
||||
dotnet_diagnostic.OSUF001.severity = none
|
||||
dotnet_diagnostic.OSUF001.severity = none
|
||||
|
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.127.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.131.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -20,5 +20,6 @@
|
||||
<file src="**.dll" target="lib\net45\"/>
|
||||
<file src="**.config" target="lib\net45\"/>
|
||||
<file src="**.json" target="lib\net45\"/>
|
||||
<file src="icon.png" target=""/>
|
||||
</files>
|
||||
</package>
|
||||
|
@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
foreach (var stage in stages)
|
||||
{
|
||||
for (int i = 0; i < stage.Columns.Count; i++)
|
||||
for (int i = 0; i < stage.Columns.Length; i++)
|
||||
{
|
||||
var obj = new Note { Column = i, StartTime = Time.Current + 2000 };
|
||||
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
foreach (var stage in stages)
|
||||
{
|
||||
for (int i = 0; i < stage.Columns.Count; i++)
|
||||
for (int i = 0; i < stage.Columns.Length; i++)
|
||||
{
|
||||
var obj = new HoldNote { Column = i, StartTime = Time.Current + 2000, Duration = 500 };
|
||||
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
@ -17,7 +17,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
{
|
||||
public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
|
||||
public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement
|
||||
{
|
||||
private const float judgement_y_position = 160;
|
||||
|
||||
|
@ -4,8 +4,6 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
@ -28,20 +26,21 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
/// <summary>
|
||||
/// All contents added to this <see cref="ColumnFlow{TContent}"/>.
|
||||
/// </summary>
|
||||
public IReadOnlyList<TContent> Content => columns.Children.Select(c => c.Count == 0 ? null : (TContent)c.Child).ToList();
|
||||
public TContent[] Content { get; }
|
||||
|
||||
private readonly FillFlowContainer<Container> columns;
|
||||
private readonly FillFlowContainer<Container<TContent>> columns;
|
||||
private readonly StageDefinition stageDefinition;
|
||||
|
||||
public ColumnFlow(StageDefinition stageDefinition)
|
||||
{
|
||||
this.stageDefinition = stageDefinition;
|
||||
Content = new TContent[stageDefinition.Columns];
|
||||
|
||||
AutoSizeAxes = Axes.X;
|
||||
|
||||
Masking = true;
|
||||
|
||||
InternalChild = columns = new FillFlowContainer<Container>
|
||||
InternalChild = columns = new FillFlowContainer<Container<TContent>>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
@ -49,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
};
|
||||
|
||||
for (int i = 0; i < stageDefinition.Columns; i++)
|
||||
columns.Add(new Container { RelativeSizeAxes = Axes.Y });
|
||||
columns.Add(new Container<TContent> { RelativeSizeAxes = Axes.Y });
|
||||
}
|
||||
|
||||
private ISkinSource currentSkin;
|
||||
@ -102,7 +101,10 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
/// </summary>
|
||||
/// <param name="column">The index of the column to set the content of.</param>
|
||||
/// <param name="content">The content.</param>
|
||||
public void SetContentForColumn(int column, TContent content) => columns[column].Child = content;
|
||||
public void SetContentForColumn(int column, TContent content)
|
||||
{
|
||||
Content[column] = columns[column].Child = content;
|
||||
}
|
||||
|
||||
private void updateMobileSizing()
|
||||
{
|
||||
|
@ -42,7 +42,16 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
}
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos));
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
{
|
||||
foreach (var s in stages)
|
||||
{
|
||||
if (s.ReceivePositionalInputAt(screenSpacePos))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public ManiaPlayfield(List<StageDefinition> stageDefinitions)
|
||||
{
|
||||
@ -71,7 +80,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
stages.Add(newStage);
|
||||
AddNested(newStage);
|
||||
|
||||
firstColumnIndex += newStage.Columns.Count;
|
||||
firstColumnIndex += newStage.Columns.Length;
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,9 +134,9 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
foreach (var stage in stages)
|
||||
{
|
||||
if (index >= stage.Columns.Count)
|
||||
if (index >= stage.Columns.Length)
|
||||
{
|
||||
index -= stage.Columns.Count;
|
||||
index -= stage.Columns.Length;
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -140,7 +149,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
/// <summary>
|
||||
/// Retrieves the total amount of columns across all stages in this playfield.
|
||||
/// </summary>
|
||||
public int TotalColumns => stages.Sum(s => s.Columns.Count);
|
||||
public int TotalColumns => stages.Sum(s => s.Columns.Length);
|
||||
|
||||
private Stage getStageByColumn(int column)
|
||||
{
|
||||
@ -148,7 +157,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
foreach (var stage in stages)
|
||||
{
|
||||
sum += stage.Columns.Count;
|
||||
sum += stage.Columns.Length;
|
||||
if (sum > column)
|
||||
return stage;
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
public const float HIT_TARGET_POSITION = 110;
|
||||
|
||||
public IReadOnlyList<Column> Columns => columnFlow.Content;
|
||||
public Column[] Columns => columnFlow.Content;
|
||||
private readonly ColumnFlow<Column> columnFlow;
|
||||
|
||||
private readonly JudgementContainer<DrawableManiaJudgement> judgements;
|
||||
@ -45,7 +44,16 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
private readonly Drawable barLineContainer;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Columns.Any(c => c.ReceivePositionalInputAt(screenSpacePos));
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
{
|
||||
foreach (var c in Columns)
|
||||
{
|
||||
if (c.ReceivePositionalInputAt(screenSpacePos))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private readonly int firstColumnIndex;
|
||||
|
||||
@ -184,13 +192,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
NewResult += OnNewResult;
|
||||
}
|
||||
|
||||
public override void Add(HitObject hitObject) => Columns.ElementAt(((ManiaHitObject)hitObject).Column - firstColumnIndex).Add(hitObject);
|
||||
public override void Add(HitObject hitObject) => Columns[((ManiaHitObject)hitObject).Column - firstColumnIndex].Add(hitObject);
|
||||
|
||||
public override bool Remove(HitObject hitObject) => Columns.ElementAt(((ManiaHitObject)hitObject).Column - firstColumnIndex).Remove(hitObject);
|
||||
public override bool Remove(HitObject hitObject) => Columns[((ManiaHitObject)hitObject).Column - firstColumnIndex].Remove(hitObject);
|
||||
|
||||
public override void Add(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Add(h);
|
||||
public override void Add(DrawableHitObject h) => Columns[((ManiaHitObject)h.HitObject).Column - firstColumnIndex].Add(h);
|
||||
|
||||
public override bool Remove(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Remove(h);
|
||||
public override bool Remove(DrawableHitObject h) => Columns[((ManiaHitObject)h.HitObject).Column - firstColumnIndex].Remove(h);
|
||||
|
||||
public void Add(BarLine barLine) => base.Add(barLine);
|
||||
|
||||
|
@ -206,7 +206,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
AddStep($"click context menu item \"{contextMenuText}\"", () =>
|
||||
{
|
||||
MenuItem item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == "Curve type")?.Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
|
||||
MenuItem item = visualiser.ContextMenuItems!.FirstOrDefault(menuItem => menuItem.Text.Value == "Curve type")?.Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
|
||||
|
||||
item?.Action.Value?.Invoke();
|
||||
});
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
new PathControlPoint(new Vector2(-128, 0), PathType.LINEAR) // absolute position: (0, 128)
|
||||
}
|
||||
},
|
||||
RepeatCount = 1
|
||||
RepeatCount = 2
|
||||
};
|
||||
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
return slider;
|
||||
@ -45,7 +45,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
OsuHitObjectGenerationUtils.ReflectHorizontallyAlongPlayfield(slider);
|
||||
|
||||
Assert.That(slider.Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 128, 128)));
|
||||
Assert.That(slider.NestedHitObjects.OfType<SliderRepeat>().Single().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 0, 128)));
|
||||
Assert.That(slider.NestedHitObjects.OfType<SliderHeadCircle>().Single().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 128, 128)));
|
||||
Assert.That(slider.NestedHitObjects.OfType<SliderRepeat>().First().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 0, 128)));
|
||||
Assert.That(slider.NestedHitObjects.OfType<SliderTailCircle>().Single().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X, 128)));
|
||||
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
|
||||
{
|
||||
new Vector2(),
|
||||
@ -62,7 +64,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(slider);
|
||||
|
||||
Assert.That(slider.Position, Is.EqualTo(new Vector2(128, OsuPlayfield.BASE_SIZE.Y - 128)));
|
||||
Assert.That(slider.NestedHitObjects.OfType<SliderRepeat>().Single().Position, Is.EqualTo(new Vector2(0, OsuPlayfield.BASE_SIZE.Y - 128)));
|
||||
Assert.That(slider.NestedHitObjects.OfType<SliderHeadCircle>().Single().Position, Is.EqualTo(new Vector2(128, OsuPlayfield.BASE_SIZE.Y - 128)));
|
||||
Assert.That(slider.NestedHitObjects.OfType<SliderRepeat>().First().Position, Is.EqualTo(new Vector2(0, OsuPlayfield.BASE_SIZE.Y - 128)));
|
||||
Assert.That(slider.NestedHitObjects.OfType<SliderTailCircle>().Single().Position, Is.EqualTo(new Vector2(0, OsuPlayfield.BASE_SIZE.Y - 128)));
|
||||
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
|
||||
{
|
||||
new Vector2(),
|
||||
@ -79,7 +83,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
OsuHitObjectGenerationUtils.FlipSliderInPlaceHorizontally(slider);
|
||||
|
||||
Assert.That(slider.Position, Is.EqualTo(new Vector2(128, 128)));
|
||||
Assert.That(slider.NestedHitObjects.OfType<SliderRepeat>().Single().Position, Is.EqualTo(new Vector2(256, 128)));
|
||||
Assert.That(slider.NestedHitObjects.OfType<SliderHeadCircle>().Single().Position, Is.EqualTo(new Vector2(128, 128)));
|
||||
Assert.That(slider.NestedHitObjects.OfType<SliderRepeat>().First().Position, Is.EqualTo(new Vector2(256, 128)));
|
||||
Assert.That(slider.NestedHitObjects.OfType<SliderTailCircle>().Single().Position, Is.EqualTo(new Vector2(256, 128)));
|
||||
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
|
||||
{
|
||||
new Vector2(),
|
||||
|
@ -183,7 +183,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
break;
|
||||
}
|
||||
|
||||
hitObjectContainer.Add(drawableObject);
|
||||
hitObjectContainer.Add(drawableObject!);
|
||||
followPointRenderer.AddFollowPoints(objects[i]);
|
||||
}
|
||||
});
|
||||
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
@ -173,6 +174,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
public IEnumerable<ISkin> AllSources => new[] { this };
|
||||
|
||||
[CanBeNull]
|
||||
public event Action SourceChanged;
|
||||
|
||||
private bool enabled = true;
|
||||
|
@ -239,11 +239,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
if (Tracking.Value && Time.Current >= HitObject.StartTime)
|
||||
{
|
||||
// keep the sliding sample playing at the current tracking position
|
||||
if (!slidingSample.IsPlaying)
|
||||
if (!slidingSample.RequestedPlaying)
|
||||
slidingSample.Play();
|
||||
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball));
|
||||
}
|
||||
else if (slidingSample.IsPlaying)
|
||||
else if (slidingSample.IsPlaying || slidingSample.RequestedPlaying)
|
||||
slidingSample.Stop();
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public const double ANIM_DURATION = 150;
|
||||
|
||||
private const float default_tick_size = 16;
|
||||
public const float DEFAULT_TICK_SIZE = 16;
|
||||
|
||||
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
|
||||
|
||||
@ -44,8 +44,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
Masking = true,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(default_tick_size),
|
||||
BorderThickness = default_tick_size / 4,
|
||||
Size = new Vector2(DEFAULT_TICK_SIZE),
|
||||
BorderThickness = DEFAULT_TICK_SIZE / 4,
|
||||
BorderColour = Color4.White,
|
||||
Child = new Box
|
||||
{
|
||||
@ -88,8 +88,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
break;
|
||||
|
||||
case ArmedState.Miss:
|
||||
this.FadeOut(ANIM_DURATION);
|
||||
this.TransformBindableTo(AccentColour, Color4.Red, 0);
|
||||
this.FadeOut(ANIM_DURATION, Easing.OutQuint);
|
||||
break;
|
||||
|
||||
case ArmedState.Hit:
|
||||
|
@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
if (tracking.NewValue)
|
||||
{
|
||||
if (!spinningSample.IsPlaying)
|
||||
if (!spinningSample.RequestedPlaying)
|
||||
spinningSample.Play();
|
||||
|
||||
spinningSample.VolumeTo(1, 300);
|
||||
|
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
set
|
||||
{
|
||||
repeatCount = value;
|
||||
endPositionCache.Invalidate();
|
||||
updateNestedPositions();
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
public Slider()
|
||||
{
|
||||
SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples();
|
||||
Path.Version.ValueChanged += _ => endPositionCache.Invalidate();
|
||||
Path.Version.ValueChanged += _ => updateNestedPositions();
|
||||
}
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
|
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
base.ReloadMappings(realmKeyBindings);
|
||||
|
||||
if (!AllowGameplayInputs)
|
||||
KeyBindings = KeyBindings.Where(b => b.GetAction<OsuAction>() == OsuAction.Smoke).ToList();
|
||||
KeyBindings = KeyBindings.Where(static b => b.GetAction<OsuAction>() == OsuAction.Smoke).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Argon;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Legacy;
|
||||
using osu.Game.Rulesets.Osu.Statistics;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
@ -254,6 +255,9 @@ namespace osu.Game.Rulesets.Osu
|
||||
|
||||
case ArgonSkin:
|
||||
return new OsuArgonSkinTransformer(skin);
|
||||
|
||||
case TrianglesSkin:
|
||||
return new OsuTrianglesSkinTransformer(skin);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -16,7 +16,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
|
||||
public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement
|
||||
{
|
||||
private RingExplosion? ringExplosion;
|
||||
|
||||
|
@ -0,0 +1,51 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
public partial class ArgonJudgementPieceSliderTickMiss : CompositeDrawable, IAnimatableJudgement
|
||||
{
|
||||
private readonly HitResult result;
|
||||
private Circle piece = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
public ArgonJudgementPieceSliderTickMiss(HitResult result)
|
||||
{
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(piece = new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Colour = colours.ForHitResult(result),
|
||||
Size = new Vector2(ArgonSliderScorePoint.SIZE)
|
||||
});
|
||||
}
|
||||
|
||||
public void PlayAnimation()
|
||||
{
|
||||
this.ScaleTo(1.4f);
|
||||
this.ScaleTo(1f, 150, Easing.Out);
|
||||
|
||||
this.FadeOutFromOne(600);
|
||||
}
|
||||
|
||||
public Drawable? GetAboveHitObjectsProxiedContent() => piece.CreateProxy();
|
||||
}
|
||||
}
|
@ -16,14 +16,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
private Bindable<Color4> accentColour = null!;
|
||||
|
||||
private const float size = 12;
|
||||
public const float SIZE = 12;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject hitObject)
|
||||
{
|
||||
Masking = true;
|
||||
Origin = Anchor.Centre;
|
||||
Size = new Vector2(size);
|
||||
Size = new Vector2(SIZE);
|
||||
BorderThickness = 3;
|
||||
BorderColour = Color4.White;
|
||||
Child = new Box
|
||||
|
@ -19,11 +19,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
switch (lookup)
|
||||
{
|
||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
||||
HitResult result = resultComponent.Component;
|
||||
|
||||
// This should eventually be moved to a skin setting, when supported.
|
||||
if (Skin is ArgonProSkin && (resultComponent.Component == HitResult.Great || resultComponent.Component == HitResult.Perfect))
|
||||
if (Skin is ArgonProSkin && (result == HitResult.Great || result == HitResult.Perfect))
|
||||
return Drawable.Empty();
|
||||
|
||||
return new ArgonJudgementPiece(resultComponent.Component);
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.IgnoreMiss:
|
||||
case HitResult.LargeTickMiss:
|
||||
return new ArgonJudgementPieceSliderTickMiss(result);
|
||||
|
||||
default:
|
||||
return new ArgonJudgementPiece(result);
|
||||
}
|
||||
|
||||
case OsuSkinComponentLookup osuComponent:
|
||||
// TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries.
|
||||
|
@ -0,0 +1,52 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
{
|
||||
public partial class DefaultJudgementPieceSliderTickMiss : CompositeDrawable, IAnimatableJudgement
|
||||
{
|
||||
private readonly HitResult result;
|
||||
private Circle piece = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
public DefaultJudgementPieceSliderTickMiss(HitResult result)
|
||||
{
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(piece = new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Colour = colours.ForHitResult(result),
|
||||
Size = new Vector2(DrawableSliderTick.DEFAULT_TICK_SIZE)
|
||||
});
|
||||
}
|
||||
|
||||
public void PlayAnimation()
|
||||
{
|
||||
this.ScaleTo(1.4f);
|
||||
this.ScaleTo(1f, 150, Easing.Out);
|
||||
|
||||
this.FadeOutFromOne(600);
|
||||
}
|
||||
|
||||
public Drawable? GetAboveHitObjectsProxiedContent() => piece.CreateProxy();
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
// 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.Osu.Skinning.Default
|
||||
{
|
||||
public class OsuTrianglesSkinTransformer : SkinTransformer
|
||||
{
|
||||
public OsuTrianglesSkinTransformer(ISkin skin)
|
||||
: base(skin)
|
||||
{
|
||||
}
|
||||
|
||||
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
||||
HitResult result = resultComponent.Component;
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.IgnoreMiss:
|
||||
case HitResult.LargeTickMiss:
|
||||
// use argon judgement piece for new tick misses because i don't want to design another one for triangles.
|
||||
return new DefaultJudgementPieceSliderTickMiss(result);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return base.GetDrawableComponent(lookup);
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
{
|
||||
public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
|
||||
public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement
|
||||
{
|
||||
private RingExplosion? ringExplosion;
|
||||
|
||||
|
@ -16,7 +16,6 @@ using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Replays;
|
||||
using osu.Game.Rulesets.Timing;
|
||||
@ -36,6 +35,8 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
|
||||
|
||||
protected new TaikoPlayfieldAdjustmentContainer PlayfieldAdjustmentContainer => (TaikoPlayfieldAdjustmentContainer)base.PlayfieldAdjustmentContainer;
|
||||
|
||||
protected override bool UserScrollSpeedAdjustment => false;
|
||||
|
||||
private SkinnableDrawable scroller;
|
||||
@ -68,22 +69,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
TimeRange.Value = ComputeTimeRange();
|
||||
}
|
||||
|
||||
protected virtual double ComputeTimeRange()
|
||||
{
|
||||
// Taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
|
||||
const float scroll_rate = 10;
|
||||
|
||||
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
|
||||
// Width is used because it defines how many notes fit on the playfield.
|
||||
// We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default.
|
||||
float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT);
|
||||
|
||||
// Stable internally increased the slider velocity of objects by a factor of `VELOCITY_MULTIPLIER`.
|
||||
// To simulate this, we shrink the time range by that factor here.
|
||||
// This, when combined with the rest of the scrolling ruleset machinery (see `MultiplierControlPoint` et al.),
|
||||
// has the effect of increasing each multiplier control point's multiplier by `VELOCITY_MULTIPLIER`, ensuring parity with stable.
|
||||
return (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate / TaikoBeatmapConverter.VELOCITY_MULTIPLIER;
|
||||
}
|
||||
protected virtual double ComputeTimeRange() => PlayfieldAdjustmentContainer.ComputeTimeRange();
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
|
@ -179,10 +179,9 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
TaikoAction taikoAction = getTaikoActionFromPosition(position);
|
||||
|
||||
// Not too sure how this can happen, but let's avoid throwing.
|
||||
if (trackedActions.ContainsKey(source))
|
||||
if (!trackedActions.TryAdd(source, taikoAction))
|
||||
return;
|
||||
|
||||
trackedActions.Add(source, taikoAction);
|
||||
keyBindingContainer.TriggerPressed(taikoAction);
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
@ -14,6 +15,8 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
public const float MAXIMUM_ASPECT = 16f / 9f;
|
||||
public const float MINIMUM_ASPECT = 5f / 4f;
|
||||
|
||||
private const float stable_gamefield_height = 480f;
|
||||
|
||||
public readonly IBindable<bool> LockPlayfieldAspectRange = new BindableBool(true);
|
||||
|
||||
public TaikoPlayfieldAdjustmentContainer()
|
||||
@ -21,6 +24,9 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
RelativeSizeAxes = Axes.X;
|
||||
RelativePositionAxes = Axes.Y;
|
||||
Height = TaikoPlayfield.BASE_HEIGHT;
|
||||
|
||||
// Matches stable, see https://github.com/peppy/osu-stable-reference/blob/7519cafd1823f1879c0d9c991ba0e5c7fd3bfa02/osu!/GameModes/Play/Rulesets/Taiko/RulesetTaiko.cs#L514
|
||||
Y = 135f / stable_gamefield_height;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@ -28,8 +34,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
base.Update();
|
||||
|
||||
const float base_relative_height = TaikoPlayfield.BASE_HEIGHT / 768;
|
||||
// Matches stable, see https://github.com/peppy/osu-stable-reference/blob/7519cafd1823f1879c0d9c991ba0e5c7fd3bfa02/osu!/GameModes/Play/Rulesets/Taiko/RulesetTaiko.cs#L514
|
||||
const float base_position = 135f / 480f;
|
||||
|
||||
float relativeHeight = base_relative_height;
|
||||
|
||||
@ -51,10 +55,38 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
// Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions.
|
||||
relativeHeight = Math.Min(relativeHeight, 1f / 3f);
|
||||
|
||||
Y = base_position;
|
||||
|
||||
Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f));
|
||||
Width = 1 / Scale.X;
|
||||
}
|
||||
|
||||
public double ComputeTimeRange()
|
||||
{
|
||||
float currentAspect = Parent!.ChildSize.X / Parent!.ChildSize.Y;
|
||||
|
||||
if (LockPlayfieldAspectRange.Value)
|
||||
currentAspect = Math.Clamp(currentAspect, MINIMUM_ASPECT, MAXIMUM_ASPECT);
|
||||
|
||||
// in a game resolution of 1024x768, stable's scrolling system consists of objects being placed 600px (widthScaled - 40) away from their hit location.
|
||||
// however, the point at which the object renders at the end of the screen is exactly x=640, but stable makes the object start moving from beyond the screen instead of the boundary point.
|
||||
// therefore, in lazer we have to adjust the "in length" so that it's in a 640px->160px fashion before passing it down as a "time range".
|
||||
// see stable's "in length": https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManagerTaiko.cs#L168
|
||||
const float stable_hit_location = 160f;
|
||||
float widthScaled = currentAspect * stable_gamefield_height;
|
||||
float inLength = widthScaled - stable_hit_location;
|
||||
|
||||
// also in a game resolution of 1024x768, stable makes hit objects scroll from 760px->160px at a duration of 6000ms, divided by slider velocity (i.e. at a rate of 0.1px/ms)
|
||||
// compare: https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManagerTaiko.cs#L218
|
||||
// note: the variable "sv", in the linked reference, is equivalent to MultiplierControlPoint.Multiplier * 100, but since time range is agnostic of velocity, we replace "sv" with 100 below.
|
||||
float inMsLength = inLength / 100 * 1000;
|
||||
|
||||
// stable multiplies the slider velocity by 1.4x for certain reasons, divide the time range by that factor to achieve similar result.
|
||||
// for references on how the factor is applied to the time range, see:
|
||||
// 1. https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManagerTaiko.cs#L79 (DifficultySliderMultiplier multiplied by 1.4x)
|
||||
// 2. https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManager.cs#L468-L470 (DifficultySliderMultiplier used to calculate SliderScoringPointDistance)
|
||||
// 3. https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManager.cs#L248-L250 (SliderScoringPointDistance used to calculate slider velocity, i.e. the "sv" variable from above)
|
||||
inMsLength /= TaikoBeatmapConverter.VELOCITY_MULTIPLIER;
|
||||
|
||||
return inMsLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -98,9 +99,10 @@ namespace osu.Game.Tests.Beatmaps
|
||||
Beatmap = beatmap;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0067
|
||||
[CanBeNull]
|
||||
public event Action<HitObject, IEnumerable<HitObject>> ObjectConverted;
|
||||
|
||||
protected virtual void OnObjectConverted(HitObject arg1, IEnumerable<HitObject> arg2) => ObjectConverted?.Invoke(arg1, arg2);
|
||||
#pragma warning restore CS0067
|
||||
|
||||
public IBeatmap Beatmap { get; }
|
||||
|
||||
|
@ -75,8 +75,6 @@ namespace osu.Game.Tests.Chat
|
||||
return false;
|
||||
};
|
||||
});
|
||||
|
||||
AddUntilStep("wait for notifications client", () => channelManager.NotificationsConnected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -79,7 +79,7 @@ namespace osu.Game.Tests.Database
|
||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
|
||||
|
||||
// Availability is updated on construction of a RealmRulesetStore
|
||||
var _ = new RealmRulesetStore(realm, storage);
|
||||
_ = new RealmRulesetStore(realm, storage);
|
||||
|
||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.False);
|
||||
});
|
||||
@ -104,13 +104,13 @@ namespace osu.Game.Tests.Database
|
||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
|
||||
|
||||
// Availability is updated on construction of a RealmRulesetStore
|
||||
var _ = new RealmRulesetStore(realm, storage);
|
||||
_ = new RealmRulesetStore(realm, storage);
|
||||
|
||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.False);
|
||||
|
||||
// Simulate the ruleset getting updated
|
||||
LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION;
|
||||
var __ = new RealmRulesetStore(realm, storage);
|
||||
_ = new RealmRulesetStore(realm, storage);
|
||||
|
||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
|
||||
});
|
||||
|
@ -203,9 +203,9 @@ namespace osu.Game.Tests.Gameplay
|
||||
|
||||
public IRenderer Renderer => host.Renderer;
|
||||
public AudioManager AudioManager => Audio;
|
||||
public IResourceStore<byte[]> Files => null;
|
||||
public IResourceStore<byte[]> Files => null!;
|
||||
public new IResourceStore<byte[]> Resources => base.Resources;
|
||||
public RealmAccess RealmAccess => null;
|
||||
public RealmAccess RealmAccess => null!;
|
||||
public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => null;
|
||||
|
||||
#endregion
|
||||
|
@ -169,9 +169,9 @@ namespace osu.Game.Tests.NonVisual.Skinning
|
||||
|
||||
public IRenderer Renderer => new DummyRenderer();
|
||||
public AudioManager AudioManager => null;
|
||||
public IResourceStore<byte[]> Files => null;
|
||||
public IResourceStore<byte[]> Resources => null;
|
||||
public RealmAccess RealmAccess => null;
|
||||
public IResourceStore<byte[]> Files => null!;
|
||||
public IResourceStore<byte[]> Resources => null!;
|
||||
public RealmAccess RealmAccess => null!;
|
||||
public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => textureStore;
|
||||
}
|
||||
}
|
||||
|
@ -56,9 +56,9 @@ namespace osu.Game.Tests.Rulesets
|
||||
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[] { null };
|
||||
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => null;
|
||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null;
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null;
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => null!;
|
||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!;
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!;
|
||||
}
|
||||
|
||||
private class TestAPIIncompatibleRuleset : Ruleset
|
||||
@ -69,9 +69,9 @@ namespace osu.Game.Tests.Rulesets
|
||||
// simulate API incompatibility by throwing similar exceptions.
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new MissingMethodException();
|
||||
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => null;
|
||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null;
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null;
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => null!;
|
||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!;
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -142,7 +142,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
AddStep("dismiss prompt", () =>
|
||||
{
|
||||
var button = DialogOverlay.CurrentDialog.Buttons.Last();
|
||||
var button = DialogOverlay.CurrentDialog!.Buttons.Last();
|
||||
InputManager.MoveMouseTo(button);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
});
|
||||
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
|
||||
|
||||
AddStep("save changes", () => DialogOverlay.CurrentDialog.PerformOkAction());
|
||||
AddStep("save changes", () => DialogOverlay.CurrentDialog!.PerformOkAction());
|
||||
|
||||
EditorPlayer editorPlayer = null;
|
||||
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
|
||||
|
@ -47,8 +47,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
seekTo(referenceBeatmap.HitObjects[^1].GetEndTime());
|
||||
AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true);
|
||||
|
||||
AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100);
|
||||
AddAssert("score has no misses", () => getResultsScreen().Score.Statistics[HitResult.Miss] == 0);
|
||||
AddAssert("score has combo", () => getResultsScreen().Score!.Combo > 100);
|
||||
AddAssert("score has no misses", () => getResultsScreen().Score!.Statistics[HitResult.Miss] == 0);
|
||||
|
||||
AddUntilStep("avatar displayed", () => getAvatar() != null);
|
||||
AddAssert("avatar not clickable", () => getAvatar().ChildrenOfType<OsuClickableContainer>().First().Action == null);
|
||||
|
@ -138,8 +138,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
|
||||
|
||||
// Player creates new instance of mods after gameplay to ensure any runtime references to drawables etc. are not retained.
|
||||
AddAssert("results screen score has copied mods", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.Not.SameAs(playerMods.First()));
|
||||
AddAssert("results screen score has matching", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.EqualTo(playerMods.First()));
|
||||
AddAssert("results screen score has copied mods", () => (Player.GetChildScreen() as ResultsScreen)?.Score?.Mods.First(), () => Is.Not.SameAs(playerMods.First()));
|
||||
AddAssert("results screen score has matching", () => (Player.GetChildScreen() as ResultsScreen)?.Score?.Mods.First(), () => Is.EqualTo(playerMods.First()));
|
||||
|
||||
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
|
||||
AddUntilStep("databased score has correct mods", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID))!.Mods.First(), () => Is.EqualTo(playerMods.First()));
|
||||
@ -184,7 +184,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
CreateTest();
|
||||
|
||||
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
|
||||
AddStep("log back in", () => API.Login("username", "password"));
|
||||
AddStep("log back in", () =>
|
||||
{
|
||||
API.Login("username", "password");
|
||||
((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
|
||||
});
|
||||
|
||||
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
|
||||
|
||||
|
@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
playbackManager?.ReplayInputHandler.SetFrameFromTime(Time.Current - 100);
|
||||
playbackManager?.ReplayInputHandler?.SetFrameFromTime(Time.Current - 100);
|
||||
}
|
||||
|
||||
[TearDownSteps]
|
||||
|
@ -198,7 +198,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
foreach (var legacyFrame in frames.Frames)
|
||||
{
|
||||
var frame = new TestReplayFrame();
|
||||
frame.FromLegacy(legacyFrame, null);
|
||||
frame.FromLegacy(legacyFrame, null!);
|
||||
playbackReplay.Frames.Add(frame);
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
@ -9,7 +10,9 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Login;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -18,6 +21,8 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
[TestFixture]
|
||||
public partial class TestSceneLoginOverlay : OsuManualInputManagerTestScene
|
||||
{
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
private LoginOverlay loginOverlay = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -40,9 +45,69 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
public void TestLoginSuccess()
|
||||
{
|
||||
AddStep("logout", () => API.Logout());
|
||||
assertAPIState(APIState.Offline);
|
||||
|
||||
AddStep("enter password", () => loginOverlay.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
|
||||
AddStep("submit", () => loginOverlay.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
|
||||
|
||||
assertAPIState(APIState.RequiresSecondFactorAuth);
|
||||
AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType<SecondFactorAuthForm>().SingleOrDefault(), () => Is.Not.Null);
|
||||
|
||||
AddStep("set up verification handling", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case VerifySessionRequest verifySessionRequest:
|
||||
if (verifySessionRequest.VerificationKey == "88800088")
|
||||
verifySessionRequest.TriggerSuccess();
|
||||
else
|
||||
verifySessionRequest.TriggerFailure(new WebException());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "88800088");
|
||||
assertAPIState(APIState.Online);
|
||||
AddStep("clear handler", () => dummyAPI.HandleRequest = null);
|
||||
}
|
||||
|
||||
private void assertAPIState(APIState expected) =>
|
||||
AddUntilStep($"login state is {expected}", () => API.State.Value, () => Is.EqualTo(expected));
|
||||
|
||||
[Test]
|
||||
public void TestVerificationFailure()
|
||||
{
|
||||
bool verificationHandled = false;
|
||||
AddStep("reset flag", () => verificationHandled = false);
|
||||
AddStep("logout", () => API.Logout());
|
||||
assertAPIState(APIState.Offline);
|
||||
|
||||
AddStep("enter password", () => loginOverlay.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
|
||||
AddStep("submit", () => loginOverlay.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
|
||||
|
||||
assertAPIState(APIState.RequiresSecondFactorAuth);
|
||||
AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType<SecondFactorAuthForm>().SingleOrDefault(), () => Is.Not.Null);
|
||||
|
||||
AddStep("set up verification handling", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case VerifySessionRequest verifySessionRequest:
|
||||
if (verifySessionRequest.VerificationKey == "88800088")
|
||||
verifySessionRequest.TriggerSuccess();
|
||||
else
|
||||
verifySessionRequest.TriggerFailure(new WebException());
|
||||
verificationHandled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "abcdefgh");
|
||||
AddUntilStep("wait for verification handled", () => verificationHandled);
|
||||
assertAPIState(APIState.RequiresSecondFactorAuth);
|
||||
AddStep("clear handler", () => dummyAPI.HandleRequest = null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -78,6 +143,12 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
AddStep("enter password", () => loginOverlay.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
|
||||
AddStep("submit", () => loginOverlay.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
|
||||
|
||||
assertAPIState(APIState.RequiresSecondFactorAuth);
|
||||
AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType<SecondFactorAuthForm>().SingleOrDefault(), () => Is.Not.Null);
|
||||
|
||||
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "88800088");
|
||||
assertAPIState(APIState.Online);
|
||||
|
||||
AddStep("click on flag", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<UpdateableFlag>().First());
|
||||
|
@ -250,7 +250,7 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
{
|
||||
}
|
||||
|
||||
public virtual IBindable<int> UnreadCount => null;
|
||||
public virtual IBindable<int> UnreadCount { get; } = new Bindable<int>();
|
||||
|
||||
public IEnumerable<Notification> AllNotifications => Enumerable.Empty<Notification>();
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
[TestFixture]
|
||||
public partial class TestSceneToolbarUserButton : OsuManualInputManagerTestScene
|
||||
{
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
public TestSceneToolbarUserButton()
|
||||
{
|
||||
Container mainContainer;
|
||||
@ -69,18 +71,20 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
[Test]
|
||||
public void TestLoginLogout()
|
||||
{
|
||||
AddStep("Log out", () => ((DummyAPIAccess)API).Logout());
|
||||
AddStep("Log in", () => ((DummyAPIAccess)API).Login("wang", "jang"));
|
||||
AddStep("Log out", () => dummyAPI.Logout());
|
||||
AddStep("Log in", () => dummyAPI.Login("wang", "jang"));
|
||||
AddStep("Authenticate via second factor", () => dummyAPI.AuthenticateSecondFactor("abcdefgh"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStates()
|
||||
{
|
||||
AddStep("Log in", () => ((DummyAPIAccess)API).Login("wang", "jang"));
|
||||
AddStep("Log in", () => dummyAPI.Login("wang", "jang"));
|
||||
AddStep("Authenticate via second factor", () => dummyAPI.AuthenticateSecondFactor("abcdefgh"));
|
||||
|
||||
foreach (var state in Enum.GetValues<APIState>())
|
||||
{
|
||||
AddStep($"Change state to {state}", () => ((DummyAPIAccess)API).SetState(state));
|
||||
AddStep($"Change state to {state}", () => dummyAPI.SetState(state));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
.SkipWhile(r => r.Room.Category.Value == RoomCategory.Spotlight)
|
||||
.All(r => r.Room.Category.Value == RoomCategory.Normal));
|
||||
|
||||
AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.FirstOrDefault(r => r.RoomID.Value == 0)));
|
||||
AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.First(r => r.RoomID.Value == 0)));
|
||||
AddAssert("has 4 rooms", () => container.Rooms.Count == 4);
|
||||
AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0));
|
||||
|
||||
|
@ -698,7 +698,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
var scoreInfo = ((ResultsScreen)multiplayerComponents.CurrentScreen).Score;
|
||||
|
||||
return !scoreInfo.Passed && scoreInfo.Rank == ScoreRank.F;
|
||||
return scoreInfo?.Passed == false && scoreInfo.Rank == ScoreRank.F;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -3,13 +3,18 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
@ -19,14 +24,37 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
private MultiplayerPlayer player;
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
[Test]
|
||||
public void TestGameplay()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
setup();
|
||||
|
||||
AddUntilStep("wait for gameplay start", () => player.LocalUserPlaying.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFail()
|
||||
{
|
||||
setup(() => new[] { new OsuModAutopilot() });
|
||||
|
||||
AddUntilStep("wait for gameplay start", () => player.LocalUserPlaying.Value);
|
||||
AddStep("set health zero", () => player.ChildrenOfType<HealthProcessor>().Single().Health.Value = 0);
|
||||
AddUntilStep("wait for fail", () => player.ChildrenOfType<HealthProcessor>().Single().HasFailed);
|
||||
AddAssert("fail animation not shown", () => !player.GameplayState.HasFailed);
|
||||
|
||||
// ensure that even after reaching a failed state, score processor keeps accounting for new hit results.
|
||||
// the testing method used here (autopilot + hold key) is sort-of dodgy, but works enough.
|
||||
AddAssert("score is zero", () => player.GameplayState.ScoreProcessor.TotalScore.Value == 0);
|
||||
AddStep("hold key", () => player.ChildrenOfType<OsuInputManager.RulesetKeyBindingContainer>().First().TriggerPressed(OsuAction.LeftButton));
|
||||
AddUntilStep("score changed", () => player.GameplayState.ScoreProcessor.TotalScore.Value > 0);
|
||||
}
|
||||
|
||||
private void setup(Func<IReadOnlyList<Mod>> mods = null)
|
||||
{
|
||||
AddStep("set beatmap", () =>
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||
SelectedMods.Value = mods?.Invoke() ?? Array.Empty<Mod>();
|
||||
});
|
||||
|
||||
AddStep("Start track playing", () =>
|
||||
@ -52,11 +80,5 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("gameplay clock is not paused", () => !player.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value);
|
||||
AddAssert("gameplay clock is running", () => player.ChildrenOfType<GameplayClockContainer>().Single().IsRunning);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGameplay()
|
||||
{
|
||||
AddUntilStep("wait for gameplay start", () => player.LocalUserPlaying.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
case ScorePresentType.Results:
|
||||
AddUntilStep("wait for results", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ResultsScreen);
|
||||
AddStep("store last waited screen", () => lastWaitedScreen = Game.ScreenStack.CurrentScreen);
|
||||
AddUntilStep("correct score displayed", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score.Equals(getImport()));
|
||||
AddUntilStep("correct score displayed", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score!.Equals(getImport()));
|
||||
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.Equals(getImport().Ruleset));
|
||||
break;
|
||||
|
||||
|
@ -10,6 +10,8 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.AccountCreation;
|
||||
@ -59,7 +61,40 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddStep("click button", () => accountCreation.ChildrenOfType<SettingsButton>().Single().TriggerClick());
|
||||
AddUntilStep("warning screen is present", () => accountCreation.ChildrenOfType<ScreenWarning>().SingleOrDefault()?.IsPresent == true);
|
||||
|
||||
AddStep("log back in", () => API.Login("dummy", "password"));
|
||||
AddStep("log back in", () =>
|
||||
{
|
||||
API.Login("dummy", "password");
|
||||
((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
|
||||
});
|
||||
AddUntilStep("overlay is hidden", () => accountCreation.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFullFlow()
|
||||
{
|
||||
AddStep("log out", () => API.Logout());
|
||||
|
||||
AddStep("show manually", () => accountCreation.Show());
|
||||
AddUntilStep("overlay is visible", () => accountCreation.State.Value == Visibility.Visible);
|
||||
|
||||
AddStep("click button", () => accountCreation.ChildrenOfType<SettingsButton>().Single().TriggerClick());
|
||||
AddUntilStep("warning screen is present", () => accountCreation.ChildrenOfType<ScreenWarning>().SingleOrDefault()?.IsPresent == true);
|
||||
|
||||
AddStep("proceed", () => accountCreation.ChildrenOfType<DangerousSettingsButton>().Single().TriggerClick());
|
||||
AddUntilStep("entry screen is present", () => accountCreation.ChildrenOfType<ScreenEntry>().SingleOrDefault()?.IsPresent == true);
|
||||
|
||||
AddStep("input details", () =>
|
||||
{
|
||||
var entryScreen = accountCreation.ChildrenOfType<ScreenEntry>().Single();
|
||||
entryScreen.ChildrenOfType<OsuTextBox>().ElementAt(0).Text = "new_user";
|
||||
entryScreen.ChildrenOfType<OsuTextBox>().ElementAt(1).Text = "new.user@fake.mail";
|
||||
entryScreen.ChildrenOfType<OsuTextBox>().ElementAt(2).Text = "password";
|
||||
});
|
||||
AddStep("click button", () => accountCreation.ChildrenOfType<ScreenEntry>().Single()
|
||||
.ChildrenOfType<SettingsButton>().Single().TriggerClick());
|
||||
AddUntilStep("verification screen is present", () => accountCreation.ChildrenOfType<ScreenEmailVerification>().SingleOrDefault()?.IsPresent == true);
|
||||
|
||||
AddStep("verify", () => ((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh"));
|
||||
AddUntilStep("overlay is hidden", () => accountCreation.State.Value == Visibility.Hidden);
|
||||
}
|
||||
}
|
||||
|
@ -67,6 +67,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Schedule(() =>
|
||||
{
|
||||
API.Login("test", "test");
|
||||
dummyAPI.AuthenticateSecondFactor("abcdefgh");
|
||||
Child = commentsContainer = new CommentsContainer();
|
||||
});
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.BeatmapSet.Buttons;
|
||||
using osuTK;
|
||||
@ -34,14 +35,22 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddStep("set valid beatmap", () => favourite.BeatmapSet.Value = new APIBeatmapSet { OnlineID = 88 });
|
||||
AddStep("log out", () => API.Logout());
|
||||
checkEnabled(false);
|
||||
AddStep("log in", () => API.Login("test", "test"));
|
||||
AddStep("log in", () =>
|
||||
{
|
||||
API.Login("test", "test");
|
||||
((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
|
||||
});
|
||||
checkEnabled(true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBeatmapChange()
|
||||
{
|
||||
AddStep("log in", () => API.Login("test", "test"));
|
||||
AddStep("log in", () =>
|
||||
{
|
||||
API.Login("test", "test");
|
||||
((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
|
||||
});
|
||||
AddStep("set valid beatmap", () => favourite.BeatmapSet.Value = new APIBeatmapSet { OnlineID = 88 });
|
||||
checkEnabled(true);
|
||||
AddStep("set invalid beatmap", () => favourite.BeatmapSet.Value = new APIBeatmapSet());
|
||||
|
@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
else
|
||||
{
|
||||
int userId = int.Parse(getUserRequest.Lookup);
|
||||
string rulesetName = getUserRequest.Ruleset.ShortName;
|
||||
string rulesetName = getUserRequest.Ruleset!.ShortName;
|
||||
var response = new APIUser
|
||||
{
|
||||
Id = userId,
|
||||
@ -177,7 +177,11 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddWaitStep("wait a bit", 5);
|
||||
AddAssert("update not received", () => update == null);
|
||||
|
||||
AddStep("log in user", () => dummyAPI.Login("user", "password"));
|
||||
AddStep("log in user", () =>
|
||||
{
|
||||
dummyAPI.Login("user", "password");
|
||||
dummyAPI.AuthenticateSecondFactor("abcdefgh");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -52,7 +52,11 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
|
||||
AddToggleStep("toggle visibility", visible => profile.State.Value = visible ? Visibility.Visible : Visibility.Hidden);
|
||||
AddStep("log out", () => dummyAPI.Logout());
|
||||
AddStep("log back in", () => dummyAPI.Login("username", "password"));
|
||||
AddStep("log back in", () =>
|
||||
{
|
||||
dummyAPI.Login("username", "password");
|
||||
dummyAPI.AuthenticateSecondFactor("abcdefgh");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -98,7 +102,11 @@ namespace osu.Game.Tests.Visual.Online
|
||||
});
|
||||
AddStep("logout", () => dummyAPI.Logout());
|
||||
AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
|
||||
AddStep("login", () => dummyAPI.Login("username", "password"));
|
||||
AddStep("login", () =>
|
||||
{
|
||||
dummyAPI.Login("username", "password");
|
||||
dummyAPI.AuthenticateSecondFactor("abcdefgh");
|
||||
});
|
||||
AddWaitStep("wait some", 3);
|
||||
AddStep("complete request", () => pendingRequest.TriggerSuccess(TEST_USER));
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.API;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
@ -72,7 +73,11 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddAssert("Login overlay is visible", () => login.State.Value == Visibility.Visible);
|
||||
}
|
||||
|
||||
private void logIn() => API.Login("localUser", "password");
|
||||
private void logIn()
|
||||
{
|
||||
API.Login("localUser", "password");
|
||||
((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
|
||||
}
|
||||
|
||||
private Comment getUserComment() => new Comment
|
||||
{
|
||||
|
@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
|
||||
public IBindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
|
||||
|
||||
public IBindableList<Room> Rooms => null;
|
||||
public IBindableList<Room> Rooms => null!;
|
||||
|
||||
public void AddOrUpdateRoom(Room room) => throw new NotImplementedException();
|
||||
|
||||
|
@ -420,7 +420,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
public new LoadingSpinner RightSpinner => base.RightSpinner;
|
||||
public new ScorePanelList ScorePanelList => base.ScorePanelList;
|
||||
|
||||
public TestResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
|
||||
public TestResultsScreen([CanBeNull] ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
|
||||
: base(score, roomId, playlistItem, allowRetry)
|
||||
{
|
||||
}
|
||||
|
@ -52,11 +52,11 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
[SetUpSteps]
|
||||
public void SetupSteps()
|
||||
{
|
||||
AddStep("set room", () => SelectedRoom.Value = new Room());
|
||||
AddStep("set room", () => SelectedRoom!.Value = new Room());
|
||||
|
||||
importBeatmap();
|
||||
|
||||
AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(SelectedRoom.Value)));
|
||||
AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(SelectedRoom!.Value)));
|
||||
AddUntilStep("wait for load", () => match.IsCurrentScreen());
|
||||
}
|
||||
|
||||
@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
});
|
||||
});
|
||||
|
||||
AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom.Value.Playlist[0]);
|
||||
AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom!.Value.Playlist[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -201,7 +201,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
|
||||
private void setupAndCreateRoom(Action<Room> room)
|
||||
{
|
||||
AddStep("setup room", () => room(SelectedRoom.Value));
|
||||
AddStep("setup room", () => room(SelectedRoom!.Value));
|
||||
|
||||
AddStep("click create button", () =>
|
||||
{
|
||||
|
@ -418,7 +418,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
public UnrankedSoloResultsScreen(ScoreInfo score)
|
||||
: base(score, true)
|
||||
{
|
||||
Score.BeatmapInfo!.OnlineID = 0;
|
||||
Score!.BeatmapInfo!.OnlineID = 0;
|
||||
Score.BeatmapInfo.Status = BeatmapOnlineStatus.Pending;
|
||||
}
|
||||
|
||||
@ -432,7 +432,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
|
||||
private class RulesetWithNoPerformanceCalculator : OsuRuleset
|
||||
{
|
||||
public override PerformanceCalculator CreatePerformanceCalculator() => null;
|
||||
public override PerformanceCalculator CreatePerformanceCalculator() => null!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -125,7 +125,11 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
assertLoggedOutState();
|
||||
|
||||
// moving from logged out -> logged in
|
||||
AddStep("log back in", () => dummyAPI.Login("username", "password"));
|
||||
AddStep("log back in", () =>
|
||||
{
|
||||
dummyAPI.Login("username", "password");
|
||||
dummyAPI.AuthenticateSecondFactor("abcdefgh");
|
||||
});
|
||||
assertLoggedInState();
|
||||
}
|
||||
|
||||
|
@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
}
|
||||
|
||||
public virtual IBindable<int> UnreadCount => null;
|
||||
public virtual IBindable<int> UnreadCount { get; } = new Bindable<int>();
|
||||
|
||||
public IEnumerable<Notification> AllNotifications => Enumerable.Empty<Notification>();
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
public readonly BindableDouble ScrollSpeedBindable = new BindableDouble(1)
|
||||
{
|
||||
Precision = 0.01,
|
||||
MinValue = 0.01,
|
||||
MaxValue = 10
|
||||
};
|
||||
|
@ -567,10 +567,9 @@ namespace osu.Game.Beatmaps.Formats
|
||||
for (int i = pendingControlPoints.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var type = pendingControlPoints[i].GetType();
|
||||
if (pendingControlPointTypes.Contains(type))
|
||||
if (!pendingControlPointTypes.Add(type))
|
||||
continue;
|
||||
|
||||
pendingControlPointTypes.Add(type);
|
||||
beatmap.ControlPointInfo.Add(pendingControlPointsTime, pendingControlPoints[i]);
|
||||
}
|
||||
|
||||
|
@ -116,7 +116,7 @@ namespace osu.Game.Beatmaps
|
||||
ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
|
||||
IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer();
|
||||
AudioManager IStorageResourceProvider.AudioManager => audioManager;
|
||||
RealmAccess IStorageResourceProvider.RealmAccess => null;
|
||||
RealmAccess IStorageResourceProvider.RealmAccess => null!;
|
||||
IResourceStore<byte[]> IStorageResourceProvider.Files => files;
|
||||
IResourceStore<byte[]> IStorageResourceProvider.Resources => resources;
|
||||
IResourceStore<TextureUpload> IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore);
|
||||
|
@ -52,10 +52,10 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
public override void Add(T drawable)
|
||||
{
|
||||
base.Add(drawable);
|
||||
|
||||
Debug.Assert(drawable != null);
|
||||
|
||||
base.Add(drawable);
|
||||
|
||||
drawable.StateChanged += state => selectionChanged(drawable, state);
|
||||
}
|
||||
|
||||
|
@ -77,7 +77,7 @@ namespace osu.Game.Graphics
|
||||
{
|
||||
case HitResult.IgnoreMiss:
|
||||
case HitResult.SmallTickMiss:
|
||||
return Orange1;
|
||||
return Color4.Gray;
|
||||
|
||||
case HitResult.Miss:
|
||||
case HitResult.LargeTickMiss:
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
@ -48,6 +49,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
protected virtual float ChevronSize => 10;
|
||||
|
||||
[CanBeNull]
|
||||
public event Action<Visibility> StateChanged;
|
||||
|
||||
public readonly SpriteIcon Chevron;
|
||||
|
@ -41,6 +41,6 @@ namespace osu.Game.IO
|
||||
/// </summary>
|
||||
/// <param name="underlyingStore">The underlying provider of texture data (in arbitrary image formats).</param>
|
||||
/// <returns>A texture loader store.</returns>
|
||||
IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore);
|
||||
IResourceStore<TextureUpload>? CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore);
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString Connecting => new TranslatableString(getKey(@"connecting"), @"Connecting...");
|
||||
|
||||
/// <summary>
|
||||
/// "Verification required"
|
||||
/// </summary>
|
||||
public static LocalisableString VerificationRequired => new TranslatableString(getKey(@"verification_required"), @"Verification required");
|
||||
|
||||
/// <summary>
|
||||
/// "home"
|
||||
/// </summary>
|
||||
|
@ -21,7 +21,7 @@ using osu.Game.Configuration;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Notifications;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Online.Notifications.WebSocket;
|
||||
using osu.Game.Users;
|
||||
|
||||
@ -48,6 +48,8 @@ namespace osu.Game.Online.API
|
||||
|
||||
public string ProvidedUsername { get; private set; }
|
||||
|
||||
public string SecondFactorCode { get; private set; }
|
||||
|
||||
private string password;
|
||||
|
||||
public IBindable<APIUser> LocalUser => localUser;
|
||||
@ -55,6 +57,8 @@ namespace osu.Game.Online.API
|
||||
public IBindable<UserActivity> Activity => activity;
|
||||
public IBindable<UserStatistics> Statistics => statistics;
|
||||
|
||||
public INotificationsClient NotificationsClient { get; }
|
||||
|
||||
public Language Language => game.CurrentLanguage.Value;
|
||||
|
||||
private Bindable<APIUser> localUser { get; } = new Bindable<APIUser>(createGuestUser());
|
||||
@ -82,6 +86,7 @@ namespace osu.Game.Online.API
|
||||
|
||||
APIEndpointUrl = endpointConfiguration.APIEndpointUrl;
|
||||
WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl;
|
||||
NotificationsClient = setUpNotificationsClient();
|
||||
|
||||
authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl);
|
||||
log = Logger.GetLogger(LoggingTarget.Network);
|
||||
@ -114,6 +119,30 @@ namespace osu.Game.Online.API
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
private WebSocketNotificationsClientConnector setUpNotificationsClient()
|
||||
{
|
||||
var connector = new WebSocketNotificationsClientConnector(this);
|
||||
|
||||
connector.MessageReceived += msg =>
|
||||
{
|
||||
switch (msg.Event)
|
||||
{
|
||||
case @"verified":
|
||||
if (state.Value == APIState.RequiresSecondFactorAuth)
|
||||
state.Value = APIState.Online;
|
||||
break;
|
||||
|
||||
case @"logout":
|
||||
if (state.Value == APIState.Online)
|
||||
Logout();
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return connector;
|
||||
}
|
||||
|
||||
private void onTokenChanged(ValueChangedEvent<OAuthToken> e) => config.SetValue(OsuSetting.Token, config.Get<bool>(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty);
|
||||
|
||||
internal new void Schedule(Action action) => base.Schedule(action);
|
||||
@ -197,6 +226,7 @@ namespace osu.Game.Online.API
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method takes control of <see cref="state"/> and transitions from <see cref="APIState.Connecting"/> to either
|
||||
/// - <see cref="APIState.RequiresSecondFactorAuth"/> (pending 2fa)
|
||||
/// - <see cref="APIState.Online"/> (successful connection)
|
||||
/// - <see cref="APIState.Failing"/> (failed connection but retrying)
|
||||
/// - <see cref="APIState.Offline"/> (failed and can't retry, clear credentials and require user interaction)
|
||||
@ -204,8 +234,6 @@ namespace osu.Game.Online.API
|
||||
/// <returns>Whether the connection attempt was successful.</returns>
|
||||
private void attemptConnect()
|
||||
{
|
||||
state.Value = APIState.Connecting;
|
||||
|
||||
if (localUser.IsDefault)
|
||||
{
|
||||
// Show a placeholder user if saved credentials are available.
|
||||
@ -223,6 +251,7 @@ namespace osu.Game.Online.API
|
||||
|
||||
if (!authentication.HasValidAccessToken)
|
||||
{
|
||||
state.Value = APIState.Connecting;
|
||||
LastLoginError = null;
|
||||
|
||||
try
|
||||
@ -240,40 +269,79 @@ namespace osu.Game.Online.API
|
||||
}
|
||||
}
|
||||
|
||||
var userReq = new GetUserRequest();
|
||||
userReq.Failure += ex =>
|
||||
switch (state.Value)
|
||||
{
|
||||
if (ex is APIException)
|
||||
case APIState.RequiresSecondFactorAuth:
|
||||
{
|
||||
LastLoginError = ex;
|
||||
log.Add($@"Login failed for username {ProvidedUsername} on user retrieval ({LastLoginError.Message})!");
|
||||
Logout();
|
||||
if (string.IsNullOrEmpty(SecondFactorCode))
|
||||
return;
|
||||
|
||||
state.Value = APIState.Connecting;
|
||||
LastLoginError = null;
|
||||
|
||||
var verificationRequest = new VerifySessionRequest(SecondFactorCode);
|
||||
|
||||
verificationRequest.Success += () => state.Value = APIState.Online;
|
||||
verificationRequest.Failure += ex =>
|
||||
{
|
||||
state.Value = APIState.RequiresSecondFactorAuth;
|
||||
LastLoginError = ex;
|
||||
SecondFactorCode = null;
|
||||
};
|
||||
|
||||
if (!handleRequest(verificationRequest))
|
||||
{
|
||||
state.Value = APIState.Failing;
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.Value != APIState.Online)
|
||||
return;
|
||||
|
||||
break;
|
||||
}
|
||||
else if (ex is WebException webException && webException.Message == @"Unauthorized")
|
||||
|
||||
default:
|
||||
{
|
||||
log.Add(@"Login no longer valid");
|
||||
Logout();
|
||||
var userReq = new GetMeRequest();
|
||||
|
||||
userReq.Failure += ex =>
|
||||
{
|
||||
if (ex is APIException)
|
||||
{
|
||||
LastLoginError = ex;
|
||||
log.Add($@"Login failed for username {ProvidedUsername} on user retrieval ({LastLoginError.Message})!");
|
||||
Logout();
|
||||
}
|
||||
else if (ex is WebException webException && webException.Message == @"Unauthorized")
|
||||
{
|
||||
log.Add(@"Login no longer valid");
|
||||
Logout();
|
||||
}
|
||||
else
|
||||
{
|
||||
state.Value = APIState.Failing;
|
||||
}
|
||||
};
|
||||
|
||||
userReq.Success += me =>
|
||||
{
|
||||
me.Status.Value = configStatus.Value ?? UserStatus.Online;
|
||||
|
||||
setLocalUser(me);
|
||||
|
||||
state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth;
|
||||
failureCount = 0;
|
||||
};
|
||||
|
||||
if (!handleRequest(userReq))
|
||||
{
|
||||
state.Value = APIState.Failing;
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.Value = APIState.Failing;
|
||||
}
|
||||
};
|
||||
userReq.Success += user =>
|
||||
{
|
||||
user.Status.Value = configStatus.Value ?? UserStatus.Online;
|
||||
|
||||
setLocalUser(user);
|
||||
|
||||
// we're connected!
|
||||
state.Value = APIState.Online;
|
||||
failureCount = 0;
|
||||
};
|
||||
|
||||
if (!handleRequest(userReq))
|
||||
{
|
||||
state.Value = APIState.Failing;
|
||||
return;
|
||||
}
|
||||
|
||||
var friendsReq = new GetFriendsRequest();
|
||||
@ -321,11 +389,17 @@ namespace osu.Game.Online.API
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public void AuthenticateSecondFactor(string code)
|
||||
{
|
||||
Debug.Assert(State.Value == APIState.RequiresSecondFactorAuth);
|
||||
|
||||
SecondFactorCode = code;
|
||||
}
|
||||
|
||||
public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) =>
|
||||
new HubClientConnector(clientName, endpoint, this, versionHash, preferMessagePack);
|
||||
|
||||
public NotificationsClientConnector GetNotificationsConnector() =>
|
||||
new WebSocketNotificationsClientConnector(this);
|
||||
public IChatClient GetChatClient() => new WebSocketChatClient(this);
|
||||
|
||||
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
|
||||
{
|
||||
@ -507,6 +581,7 @@ namespace osu.Game.Online.API
|
||||
public void Logout()
|
||||
{
|
||||
password = null;
|
||||
SecondFactorCode = null;
|
||||
authentication.Clear();
|
||||
|
||||
// Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present
|
||||
@ -566,6 +641,11 @@ namespace osu.Game.Online.API
|
||||
/// </summary>
|
||||
Failing,
|
||||
|
||||
/// <summary>
|
||||
/// Waiting on second factor authentication.
|
||||
/// </summary>
|
||||
RequiresSecondFactorAuth,
|
||||
|
||||
/// <summary>
|
||||
/// We are in the process of (re-)connecting.
|
||||
/// </summary>
|
||||
|
@ -7,8 +7,10 @@ using System.Threading.Tasks;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Notifications;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Online.Notifications.WebSocket;
|
||||
using osu.Game.Tests;
|
||||
using osu.Game.Users;
|
||||
|
||||
@ -30,6 +32,9 @@ namespace osu.Game.Online.API
|
||||
|
||||
public Bindable<UserStatistics?> Statistics { get; } = new Bindable<UserStatistics?>();
|
||||
|
||||
public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient();
|
||||
INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient;
|
||||
|
||||
public Language Language => Language.en;
|
||||
|
||||
public string AccessToken => "token";
|
||||
@ -57,6 +62,7 @@ namespace osu.Game.Online.API
|
||||
|
||||
private bool shouldFailNextLogin;
|
||||
private bool stayConnectingNextLogin;
|
||||
private bool requiredSecondFactorAuth = true;
|
||||
|
||||
/// <summary>
|
||||
/// The current connectivity state of the API.
|
||||
@ -117,13 +123,46 @@ namespace osu.Game.Online.API
|
||||
Id = DUMMY_USER_ID,
|
||||
};
|
||||
|
||||
if (requiredSecondFactorAuth)
|
||||
{
|
||||
state.Value = APIState.RequiresSecondFactorAuth;
|
||||
}
|
||||
else
|
||||
{
|
||||
onSuccessfulLogin();
|
||||
requiredSecondFactorAuth = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void AuthenticateSecondFactor(string code)
|
||||
{
|
||||
var request = new VerifySessionRequest(code);
|
||||
request.Failure += e =>
|
||||
{
|
||||
state.Value = APIState.RequiresSecondFactorAuth;
|
||||
LastLoginError = e;
|
||||
};
|
||||
|
||||
state.Value = APIState.Connecting;
|
||||
LastLoginError = null;
|
||||
|
||||
// if no handler installed / handler can't handle verification, just assume that the server would verify for simplicity.
|
||||
if (HandleRequest?.Invoke(request) != true)
|
||||
onSuccessfulLogin();
|
||||
|
||||
// if a handler did handle this, make sure the verification actually passed.
|
||||
if (request.CompletionState == APIRequestCompletionState.Completed)
|
||||
onSuccessfulLogin();
|
||||
}
|
||||
|
||||
private void onSuccessfulLogin()
|
||||
{
|
||||
state.Value = APIState.Online;
|
||||
Statistics.Value = new UserStatistics
|
||||
{
|
||||
GlobalRank = 1,
|
||||
CountryRank = 1
|
||||
};
|
||||
|
||||
state.Value = APIState.Online;
|
||||
}
|
||||
|
||||
public void Logout()
|
||||
@ -144,7 +183,7 @@ namespace osu.Game.Online.API
|
||||
|
||||
public IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null;
|
||||
|
||||
public NotificationsClientConnector GetNotificationsConnector() => new PollingNotificationsClientConnector(this);
|
||||
public IChatClient GetChatClient() => new TestChatClientConnector(this);
|
||||
|
||||
public RegistrationRequest.RegistrationRequestErrors? CreateAccount(string email, string username, string password)
|
||||
{
|
||||
@ -159,6 +198,11 @@ namespace osu.Game.Online.API
|
||||
IBindable<UserActivity> IAPIProvider.Activity => Activity;
|
||||
IBindable<UserStatistics?> IAPIProvider.Statistics => Statistics;
|
||||
|
||||
/// <summary>
|
||||
/// Skip 2FA requirement for next login.
|
||||
/// </summary>
|
||||
public void SkipSecondFactor() => requiredSecondFactorAuth = false;
|
||||
|
||||
/// <summary>
|
||||
/// During the next simulated login, the process will fail immediately.
|
||||
/// </summary>
|
||||
|
@ -6,7 +6,8 @@ using System.Threading.Tasks;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Notifications;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Online.Notifications.WebSocket;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Online.API
|
||||
@ -111,6 +112,12 @@ namespace osu.Game.Online.API
|
||||
/// <param name="password">The user's password.</param>
|
||||
void Login(string username, string password);
|
||||
|
||||
/// <summary>
|
||||
/// Provide a second-factor authentication code for authentication.
|
||||
/// </summary>
|
||||
/// <param name="code">The 2FA code.</param>
|
||||
void AuthenticateSecondFactor(string code);
|
||||
|
||||
/// <summary>
|
||||
/// Log out the current user.
|
||||
/// </summary>
|
||||
@ -130,9 +137,14 @@ namespace osu.Game.Online.API
|
||||
IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack = true);
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new <see cref="NotificationsClientConnector"/>.
|
||||
/// Accesses the <see cref="INotificationsClient"/> used to receive asynchronous notifications from web.
|
||||
/// </summary>
|
||||
NotificationsClientConnector GetNotificationsConnector();
|
||||
INotificationsClient NotificationsClient { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="IChatClient"/> instance to use in order to chat.
|
||||
/// </summary>
|
||||
IChatClient GetChatClient();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new user account. This is a blocking operation.
|
||||
|
@ -128,19 +128,12 @@ namespace osu.Game.Online.API
|
||||
// if we already have a valid access token, let's use it.
|
||||
if (accessTokenValid) return true;
|
||||
|
||||
// we want to ensure only a single authentication update is happening at once.
|
||||
lock (access_token_retrieval_lock)
|
||||
{
|
||||
// re-check if valid, in case another request completed and revalidated our access.
|
||||
if (accessTokenValid) return true;
|
||||
// if not, let's try using our refresh token to request a new access token.
|
||||
if (!string.IsNullOrEmpty(Token.Value?.RefreshToken))
|
||||
// ReSharper disable once PossibleNullReferenceException
|
||||
AuthenticateWithRefresh(Token.Value.RefreshToken);
|
||||
|
||||
// if not, let's try using our refresh token to request a new access token.
|
||||
if (!string.IsNullOrEmpty(Token.Value?.RefreshToken))
|
||||
// ReSharper disable once PossibleNullReferenceException
|
||||
AuthenticateWithRefresh(Token.Value.RefreshToken);
|
||||
|
||||
return accessTokenValid;
|
||||
}
|
||||
return accessTokenValid;
|
||||
}
|
||||
|
||||
private bool accessTokenValid => Token.Value?.IsValid ?? false;
|
||||
@ -149,14 +142,18 @@ namespace osu.Game.Online.API
|
||||
|
||||
internal string RequestAccessToken()
|
||||
{
|
||||
if (!ensureAccessToken()) return null;
|
||||
lock (access_token_retrieval_lock)
|
||||
{
|
||||
if (!ensureAccessToken()) return null;
|
||||
|
||||
return Token.Value.AccessToken;
|
||||
return Token.Value.AccessToken;
|
||||
}
|
||||
}
|
||||
|
||||
internal void Clear()
|
||||
{
|
||||
Token.Value = null;
|
||||
lock (access_token_retrieval_lock)
|
||||
Token.Value = null;
|
||||
}
|
||||
|
||||
private class AccessTokenRequestRefresh : AccessTokenRequest
|
||||
|
24
osu.Game/Online/API/Requests/GetMeRequest.cs
Normal file
24
osu.Game/Online/API/Requests/GetMeRequest.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 osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class GetMeRequest : APIRequest<APIMe>
|
||||
{
|
||||
public readonly IRulesetInfo? Ruleset;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently logged-in user.
|
||||
/// </summary>
|
||||
/// <param name="ruleset">The ruleset to get the user's info for.</param>
|
||||
public GetMeRequest(IRulesetInfo? ruleset = null)
|
||||
{
|
||||
Ruleset = ruleset;
|
||||
}
|
||||
|
||||
protected override string Target => $@"me/{Ruleset?.ShortName}";
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
// 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.Game.Online.API.Requests.Responses;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
@ -9,7 +8,7 @@ namespace osu.Game.Online.API.Requests
|
||||
public class GetSystemTitleRequest : OsuJsonWebRequest<APISystemTitle>
|
||||
{
|
||||
public GetSystemTitleRequest()
|
||||
: base($@"https://assets.ppy.sh/lazer-status.json?{DateTimeOffset.UtcNow.ToUnixTimeSeconds() / 1800}")
|
||||
: base(@"https://assets.ppy.sh/lazer-status.json")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
// 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 osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
@ -11,24 +9,17 @@ namespace osu.Game.Online.API.Requests
|
||||
public class GetUserRequest : APIRequest<APIUser>
|
||||
{
|
||||
public readonly string Lookup;
|
||||
public readonly IRulesetInfo Ruleset;
|
||||
public readonly IRulesetInfo? Ruleset;
|
||||
private readonly LookupType lookupType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently logged-in user.
|
||||
/// </summary>
|
||||
public GetUserRequest()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a user from their ID.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user to get.</param>
|
||||
/// <param name="ruleset">The ruleset to get the user's info for.</param>
|
||||
public GetUserRequest(long? userId = null, IRulesetInfo ruleset = null)
|
||||
public GetUserRequest(long? userId = null, IRulesetInfo? ruleset = null)
|
||||
{
|
||||
Lookup = userId.ToString();
|
||||
Lookup = userId.ToString()!;
|
||||
lookupType = LookupType.Id;
|
||||
Ruleset = ruleset;
|
||||
}
|
||||
@ -38,14 +29,14 @@ namespace osu.Game.Online.API.Requests
|
||||
/// </summary>
|
||||
/// <param name="username">The user to get.</param>
|
||||
/// <param name="ruleset">The ruleset to get the user's info for.</param>
|
||||
public GetUserRequest(string username = null, IRulesetInfo ruleset = null)
|
||||
public GetUserRequest(string username, IRulesetInfo? ruleset = null)
|
||||
{
|
||||
Lookup = username;
|
||||
lookupType = LookupType.Username;
|
||||
Ruleset = ruleset;
|
||||
}
|
||||
|
||||
protected override string Target => Lookup != null ? $@"users/{Lookup}/{Ruleset?.ShortName}?key={lookupType.ToString().ToLowerInvariant()}" : $@"me/{Ruleset?.ShortName}";
|
||||
protected override string Target => $@"users/{Lookup}/{Ruleset?.ShortName}?key={lookupType.ToString().ToLowerInvariant()}";
|
||||
|
||||
private enum LookupType
|
||||
{
|
||||
|
@ -0,0 +1,22 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Net.Http;
|
||||
using osu.Framework.IO.Network;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class ReissueVerificationCodeRequest : APIRequest
|
||||
{
|
||||
protected override WebRequest CreateWebRequest()
|
||||
{
|
||||
var req = base.CreateWebRequest();
|
||||
|
||||
req.Method = HttpMethod.Post;
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
protected override string Target => @"session/verify/reissue";
|
||||
}
|
||||
}
|
13
osu.Game/Online/API/Requests/Responses/APIMe.cs
Normal file
13
osu.Game/Online/API/Requests/Responses/APIMe.cs
Normal file
@ -0,0 +1,13 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.API.Requests.Responses
|
||||
{
|
||||
public class APIMe : APIUser
|
||||
{
|
||||
[JsonProperty("session_verified")]
|
||||
public bool SessionVerified { get; set; }
|
||||
}
|
||||
}
|
30
osu.Game/Online/API/Requests/VerifySessionRequest.cs
Normal file
30
osu.Game/Online/API/Requests/VerifySessionRequest.cs
Normal file
@ -0,0 +1,30 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class VerifySessionRequest : APIRequest
|
||||
{
|
||||
public readonly string VerificationKey;
|
||||
|
||||
public VerifySessionRequest(string verificationKey)
|
||||
{
|
||||
VerificationKey = verificationKey;
|
||||
}
|
||||
|
||||
protected override WebRequest CreateWebRequest()
|
||||
{
|
||||
var req = base.CreateWebRequest();
|
||||
|
||||
req.Method = HttpMethod.Post;
|
||||
req.AddParameter(@"verification_key", VerificationKey);
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
protected override string Target => @"session/verify";
|
||||
}
|
||||
}
|
@ -16,7 +16,6 @@ using osu.Game.Database;
|
||||
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
|
||||
@ -64,13 +63,8 @@ namespace osu.Game.Online.Chat
|
||||
/// </summary>
|
||||
public IBindableList<Channel> AvailableChannels => availableChannels;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the client responsible for channel notifications is connected.
|
||||
/// </summary>
|
||||
public bool NotificationsConnected => connector.IsConnected.Value;
|
||||
|
||||
private readonly IAPIProvider api;
|
||||
private readonly NotificationsClientConnector connector;
|
||||
private readonly IChatClient chatClient;
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache users { get; set; }
|
||||
@ -85,7 +79,7 @@ namespace osu.Game.Online.Chat
|
||||
{
|
||||
this.api = api;
|
||||
|
||||
connector = api.GetNotificationsConnector();
|
||||
chatClient = api.GetChatClient();
|
||||
|
||||
CurrentChannel.ValueChanged += currentChannelChanged;
|
||||
}
|
||||
@ -93,15 +87,11 @@ namespace osu.Game.Online.Chat
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
connector.ChannelJoined += ch => Schedule(() => joinChannel(ch));
|
||||
|
||||
connector.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false));
|
||||
|
||||
connector.NewMessages += msgs => Schedule(() => addMessages(msgs));
|
||||
|
||||
connector.PresenceReceived += () => Schedule(initializeChannels);
|
||||
|
||||
connector.Start();
|
||||
chatClient.ChannelJoined += ch => Schedule(() => joinChannel(ch));
|
||||
chatClient.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false));
|
||||
chatClient.NewMessages += msgs => Schedule(() => addMessages(msgs));
|
||||
chatClient.PresenceReceived += () => Schedule(initializeChannels);
|
||||
chatClient.RequestPresence();
|
||||
|
||||
apiState.BindTo(api.State);
|
||||
apiState.BindValueChanged(_ => SendAck(), true);
|
||||
@ -655,7 +645,7 @@ namespace osu.Game.Online.Chat
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
connector?.Dispose();
|
||||
chatClient?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
39
osu.Game/Online/Chat/IChatClient.cs
Normal file
39
osu.Game/Online/Chat/IChatClient.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;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Online.Chat
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for consuming online chat.
|
||||
/// </summary>
|
||||
public interface IChatClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when a <see cref="Channel"/> has been joined.
|
||||
/// </summary>
|
||||
event Action<Channel>? ChannelJoined;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a <see cref="Channel"/> has been parted.
|
||||
/// </summary>
|
||||
event Action<Channel>? ChannelParted;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when new <see cref="Message"/>s have arrived from the server.
|
||||
/// </summary>
|
||||
event Action<List<Message>>? NewMessages;
|
||||
|
||||
/// <summary>
|
||||
/// Requests presence information from the server.
|
||||
/// </summary>
|
||||
void RequestPresence();
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the initial user presence information has been received.
|
||||
/// </summary>
|
||||
event Action? PresenceReceived;
|
||||
}
|
||||
}
|
173
osu.Game/Online/Chat/WebSocketChatClient.cs
Normal file
173
osu.Game/Online/Chat/WebSocketChatClient.cs
Normal file
@ -0,0 +1,173 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Notifications.WebSocket;
|
||||
|
||||
namespace osu.Game.Online.Chat
|
||||
{
|
||||
public class WebSocketChatClient : IChatClient
|
||||
{
|
||||
public event Action<Channel>? ChannelJoined;
|
||||
public event Action<Channel>? ChannelParted;
|
||||
public event Action<List<Message>>? NewMessages;
|
||||
public event Action? PresenceReceived;
|
||||
|
||||
private readonly IAPIProvider api;
|
||||
private readonly INotificationsClient client;
|
||||
private readonly ConcurrentDictionary<long, Channel> channelsMap = new ConcurrentDictionary<long, Channel>();
|
||||
|
||||
private CancellationTokenSource? chatStartCancellationSource;
|
||||
|
||||
public WebSocketChatClient(IAPIProvider api)
|
||||
{
|
||||
this.api = api;
|
||||
client = api.NotificationsClient;
|
||||
client.IsConnected.BindValueChanged(onConnectedChanged, true);
|
||||
}
|
||||
|
||||
private void onConnectedChanged(ValueChangedEvent<bool> connected)
|
||||
{
|
||||
if (connected.NewValue)
|
||||
{
|
||||
client.MessageReceived += onMessageReceived;
|
||||
attemptToStartChat();
|
||||
RequestPresence();
|
||||
}
|
||||
else
|
||||
chatStartCancellationSource?.Cancel();
|
||||
}
|
||||
|
||||
private void attemptToStartChat()
|
||||
{
|
||||
chatStartCancellationSource?.Cancel();
|
||||
chatStartCancellationSource = new CancellationTokenSource();
|
||||
|
||||
Task.Factory.StartNew(async () =>
|
||||
{
|
||||
while (!chatStartCancellationSource.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await client.SendAsync(new StartChatRequest()).ConfigureAwait(false);
|
||||
Logger.Log(@"Now listening to websocket chat messages.", LoggingTarget.Network);
|
||||
chatStartCancellationSource.Cancel();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($@"Could not start listening to websocket chat messages: {ex}", LoggingTarget.Network);
|
||||
await Task.Delay(5000).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}, chatStartCancellationSource.Token);
|
||||
}
|
||||
|
||||
public void RequestPresence()
|
||||
{
|
||||
var fetchReq = new GetUpdatesRequest(0);
|
||||
|
||||
fetchReq.Success += updates =>
|
||||
{
|
||||
if (updates?.Presence != null)
|
||||
{
|
||||
foreach (var channel in updates.Presence)
|
||||
joinChannel(channel);
|
||||
|
||||
handleMessages(updates.Messages);
|
||||
}
|
||||
|
||||
PresenceReceived?.Invoke();
|
||||
};
|
||||
|
||||
api.Queue(fetchReq);
|
||||
}
|
||||
|
||||
private void onMessageReceived(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);
|
||||
|
||||
joinChannel(joinedChannel);
|
||||
break;
|
||||
|
||||
case @"chat.channel.part":
|
||||
Debug.Assert(message.Data != null);
|
||||
|
||||
Channel? partedChannel = JsonConvert.DeserializeObject<Channel>(message.Data.ToString());
|
||||
Debug.Assert(partedChannel != null);
|
||||
|
||||
partChannel(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)
|
||||
postToChannel(msg);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void postToChannel(Message message)
|
||||
{
|
||||
if (channelsMap.TryGetValue(message.ChannelId, out Channel? channel))
|
||||
{
|
||||
joinChannel(channel);
|
||||
NewMessages?.Invoke(new List<Message> { message });
|
||||
return;
|
||||
}
|
||||
|
||||
var req = new GetChannelRequest(message.ChannelId);
|
||||
|
||||
req.Success += response =>
|
||||
{
|
||||
joinChannel(channelsMap[message.ChannelId] = response.Channel);
|
||||
NewMessages?.Invoke(new List<Message> { message });
|
||||
};
|
||||
req.Failure += ex => Logger.Error(ex, "Failed to join channel");
|
||||
|
||||
api.Queue(req);
|
||||
}
|
||||
|
||||
private void joinChannel(Channel ch)
|
||||
{
|
||||
ch.Joined.Value = true;
|
||||
ChannelJoined?.Invoke(ch);
|
||||
}
|
||||
|
||||
private void partChannel(Channel channel) => ChannelParted?.Invoke(channel);
|
||||
|
||||
private void handleMessages(List<Message>? messages)
|
||||
{
|
||||
if (messages == null)
|
||||
return;
|
||||
|
||||
NewMessages?.Invoke(messages);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
client.IsConnected.ValueChanged -= onConnectedChanged;
|
||||
client.MessageReceived -= onMessageReceived;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
// 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
|
||||
|
||||
namespace osu.Game.Online
|
||||
{
|
||||
/// <summary>
|
||||
@ -13,36 +11,36 @@ namespace osu.Game.Online
|
||||
/// <summary>
|
||||
/// The base URL for the website.
|
||||
/// </summary>
|
||||
public string WebsiteRootUrl { get; set; }
|
||||
public string WebsiteRootUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The endpoint for the main (osu-web) API.
|
||||
/// </summary>
|
||||
public string APIEndpointUrl { get; set; }
|
||||
public string APIEndpointUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The OAuth client secret.
|
||||
/// </summary>
|
||||
public string APIClientSecret { get; set; }
|
||||
public string APIClientSecret { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The OAuth client ID.
|
||||
/// </summary>
|
||||
public string APIClientID { get; set; }
|
||||
public string APIClientID { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The endpoint for the SignalR spectator server.
|
||||
/// </summary>
|
||||
public string SpectatorEndpointUrl { get; set; }
|
||||
public string SpectatorEndpointUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The endpoint for the SignalR multiplayer server.
|
||||
/// </summary>
|
||||
public string MultiplayerEndpointUrl { get; set; }
|
||||
public string MultiplayerEndpointUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The endpoint for the SignalR metadata server.
|
||||
/// </summary>
|
||||
public string MetadataEndpointUrl { get; set; }
|
||||
public string MetadataEndpointUrl { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Online
|
||||
{
|
||||
public class ExperimentalEndpointConfiguration : EndpointConfiguration
|
||||
{
|
||||
public ExperimentalEndpointConfiguration()
|
||||
{
|
||||
WebsiteRootUrl = @"https://osu.ppy.sh";
|
||||
APIEndpointUrl = @"https://lazer.ppy.sh";
|
||||
APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
|
||||
APIClientID = "5";
|
||||
SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator";
|
||||
MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer";
|
||||
MetadataEndpointUrl = "https://spectator.ppy.sh/metadata";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
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).ConfigureAwait(false);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Bindables;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket
|
||||
{
|
||||
public class DummyNotificationsClient : INotificationsClient
|
||||
{
|
||||
public IBindable<bool> IsConnected => new BindableBool(true);
|
||||
|
||||
public event Action<SocketMessage>? MessageReceived;
|
||||
|
||||
public Func<SocketMessage, bool>? HandleMessage;
|
||||
|
||||
public Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = default)
|
||||
{
|
||||
if (HandleMessage?.Invoke(message) != true)
|
||||
throw new InvalidOperationException($@"{nameof(DummyNotificationsClient)} cannot process this message.");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Receive(SocketMessage message) => MessageReceived?.Invoke(message);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Bindables;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// A client for asynchronous notifications sent by osu-web.
|
||||
/// </summary>
|
||||
public interface INotificationsClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this <see cref="INotificationsClient"/> is currently connected to a server.
|
||||
/// </summary>
|
||||
IBindable<bool> IsConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a new <see cref="SocketMessage"/> arrives for this client.
|
||||
/// </summary>
|
||||
event Action<SocketMessage>? MessageReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a <see cref="SocketMessage"/> to the notification server.
|
||||
/// </summary>
|
||||
Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = default);
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
@ -12,23 +11,20 @@ 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
|
||||
public class WebSocketNotificationsClient : PersistentEndpointClient
|
||||
{
|
||||
public event Action<SocketMessage>? MessageReceived;
|
||||
|
||||
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)
|
||||
public WebSocketNotificationsClient(ClientWebSocket socket, string endpoint)
|
||||
{
|
||||
this.socket = socket;
|
||||
this.endpoint = endpoint;
|
||||
@ -37,11 +33,7 @@ namespace osu.Game.Online.Notifications.WebSocket
|
||||
public override async Task ConnectAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await socket.ConnectAsync(new Uri(endpoint), cancellationToken).ConfigureAwait(false);
|
||||
await sendMessage(new StartChatRequest(), CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
runReadLoop(cancellationToken);
|
||||
|
||||
await base.ConnectAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void runReadLoop(CancellationToken cancellationToken) => Task.Run(async () =>
|
||||
@ -73,7 +65,7 @@ namespace osu.Game.Online.Notifications.WebSocket
|
||||
break;
|
||||
}
|
||||
|
||||
await onMessageReceivedAsync(message).ConfigureAwait(false);
|
||||
MessageReceived?.Invoke(message);
|
||||
}
|
||||
|
||||
break;
|
||||
@ -105,69 +97,12 @@ namespace osu.Game.Online.Notifications.WebSocket
|
||||
}
|
||||
}
|
||||
|
||||
private async Task sendMessage(SocketMessage message, CancellationToken cancellationToken)
|
||||
public async Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = default)
|
||||
{
|
||||
if (socket.State != WebSocketState.Open)
|
||||
return;
|
||||
|
||||
await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
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).ConfigureAwait(false));
|
||||
|
||||
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.ConfigureAwait(false);
|
||||
await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken ?? CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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.Net;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
@ -13,26 +14,26 @@ 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
|
||||
public class WebSocketNotificationsClientConnector : PersistentEndpointClientConnector, INotificationsClient
|
||||
{
|
||||
public event Action<SocketMessage>? MessageReceived;
|
||||
|
||||
private readonly IAPIProvider api;
|
||||
|
||||
public WebSocketNotificationsClientConnector(IAPIProvider api)
|
||||
: base(api)
|
||||
{
|
||||
this.api = api;
|
||||
Start();
|
||||
}
|
||||
|
||||
protected override async Task<NotificationsClient> BuildNotificationClientAsync(CancellationToken cancellationToken)
|
||||
protected override async Task<PersistentEndpointClient> BuildConnectionAsync(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.ConfigureAwait(false);
|
||||
// must use `PerformAsync()`, since we may not be fully online yet
|
||||
// (see `APIState.RequiresSecondFactorAuth` - in this state queued requests will not execute).
|
||||
await api.PerformAsync(req).ConfigureAwait(false);
|
||||
string endpoint = req.Response!.Endpoint;
|
||||
|
||||
ClientWebSocket socket = new ClientWebSocket();
|
||||
socket.Options.SetRequestHeader(@"Authorization", @$"Bearer {api.AccessToken}");
|
||||
@ -40,7 +41,17 @@ namespace osu.Game.Online.Notifications.WebSocket
|
||||
if (socket.Options.Proxy != null)
|
||||
socket.Options.Proxy.Credentials = CredentialCache.DefaultCredentials;
|
||||
|
||||
return new WebSocketNotificationsClient(socket, endpoint, api);
|
||||
var client = new WebSocketNotificationsClient(socket, endpoint);
|
||||
client.MessageReceived += msg => MessageReceived?.Invoke(msg);
|
||||
return client;
|
||||
}
|
||||
|
||||
public Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = default)
|
||||
{
|
||||
if (CurrentConnection is not WebSocketNotificationsClient webSocketClient)
|
||||
return Task.CompletedTask;
|
||||
|
||||
return webSocketClient.SendAsync(message, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Screens;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Notifications.WebSocket;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
@ -25,6 +26,8 @@ namespace osu.Game.Online
|
||||
{
|
||||
private readonly Func<IScreen> getCurrentScreen;
|
||||
|
||||
private INotificationsClient notificationsClient = null!;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient multiplayerClient { get; set; } = null!;
|
||||
|
||||
@ -55,9 +58,11 @@ namespace osu.Game.Online
|
||||
private void load(IAPIProvider api)
|
||||
{
|
||||
apiState = api.State.GetBoundCopy();
|
||||
notificationsClient = api.NotificationsClient;
|
||||
multiplayerState = multiplayerClient.IsConnected.GetBoundCopy();
|
||||
spectatorState = spectatorClient.IsConnected.GetBoundCopy();
|
||||
|
||||
notificationsClient.MessageReceived += notifyAboutForcedDisconnection;
|
||||
multiplayerClient.Disconnecting += notifyAboutForcedDisconnection;
|
||||
spectatorClient.Disconnecting += notifyAboutForcedDisconnection;
|
||||
metadataClient.Disconnecting += notifyAboutForcedDisconnection;
|
||||
@ -127,10 +132,27 @@ namespace osu.Game.Online
|
||||
});
|
||||
}
|
||||
|
||||
private void notifyAboutForcedDisconnection(SocketMessage obj)
|
||||
{
|
||||
if (obj.Event != @"logout") return;
|
||||
|
||||
if (userNotified) return;
|
||||
|
||||
userNotified = true;
|
||||
notificationOverlay?.Post(new SimpleErrorNotification
|
||||
{
|
||||
Icon = FontAwesome.Solid.ExclamationCircle,
|
||||
Text = "You have been logged out due to a change to your account. Please log in again."
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (notificationsClient.IsNotNull())
|
||||
notificationsClient.MessageReceived += notifyAboutForcedDisconnection;
|
||||
|
||||
if (spectatorClient.IsNotNull())
|
||||
spectatorClient.Disconnecting -= notifyAboutForcedDisconnection;
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -79,10 +80,14 @@ namespace osu.Game.Online
|
||||
|
||||
case APIState.Failing:
|
||||
case APIState.Connecting:
|
||||
case APIState.RequiresSecondFactorAuth:
|
||||
PopContentOut(Content);
|
||||
LoadingSpinner.Show();
|
||||
placeholder.FadeOut(transform_duration / 2, Easing.OutQuint);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -69,6 +69,7 @@ namespace osu.Game.Online
|
||||
break;
|
||||
|
||||
case APIState.Online:
|
||||
case APIState.RequiresSecondFactorAuth:
|
||||
await connect().ConfigureAwait(true);
|
||||
break;
|
||||
}
|
||||
@ -83,7 +84,7 @@ namespace osu.Game.Online
|
||||
|
||||
try
|
||||
{
|
||||
while (apiState.Value == APIState.Online)
|
||||
while (apiState.Value == APIState.RequiresSecondFactorAuth || apiState.Value == APIState.Online)
|
||||
{
|
||||
// ensure any previous connection was disposed.
|
||||
// this will also create a new cancellation token source.
|
||||
|
@ -264,13 +264,12 @@ namespace osu.Game.Online.Spectator
|
||||
{
|
||||
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||
|
||||
if (watchedUsersRefCounts.ContainsKey(userId))
|
||||
if (!watchedUsersRefCounts.TryAdd(userId, 1))
|
||||
{
|
||||
watchedUsersRefCounts[userId]++;
|
||||
return;
|
||||
}
|
||||
|
||||
watchedUsersRefCounts.Add(userId, 1);
|
||||
WatchUserInternal(userId);
|
||||
}
|
||||
|
||||
|
@ -102,7 +102,7 @@ namespace osu.Game
|
||||
public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild;
|
||||
|
||||
public virtual EndpointConfiguration CreateEndpoints() =>
|
||||
UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ExperimentalEndpointConfiguration();
|
||||
UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
|
||||
|
||||
public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version();
|
||||
|
||||
@ -340,10 +340,6 @@ namespace osu.Game
|
||||
dependencies.Cache(beatmapCache = new BeatmapLookupCache());
|
||||
base.Content.Add(beatmapCache);
|
||||
|
||||
var scorePerformanceManager = new ScorePerformanceCache();
|
||||
dependencies.Cache(scorePerformanceManager);
|
||||
base.Content.Add(scorePerformanceManager);
|
||||
|
||||
dependencies.CacheAs<IRulesetConfigCache>(rulesetConfigCache = new RulesetConfigCache(realm, RulesetStore));
|
||||
|
||||
var powerStatus = CreateBatteryInfo();
|
||||
|
24
osu.Game/Overlays/AccountCreation/ScreenEmailVerification.cs
Normal file
24
osu.Game/Overlays/AccountCreation/ScreenEmailVerification.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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Overlays.Login;
|
||||
|
||||
namespace osu.Game.Overlays.AccountCreation
|
||||
{
|
||||
public partial class ScreenEmailVerification : AccountCreationScreen
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = new SecondFactorAuthForm
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(20),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -28,28 +27,30 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
{
|
||||
public partial class ScreenEntry : AccountCreationScreen
|
||||
{
|
||||
private ErrorTextFlowContainer usernameDescription;
|
||||
private ErrorTextFlowContainer emailAddressDescription;
|
||||
private ErrorTextFlowContainer passwordDescription;
|
||||
private ErrorTextFlowContainer usernameDescription = null!;
|
||||
private ErrorTextFlowContainer emailAddressDescription = null!;
|
||||
private ErrorTextFlowContainer passwordDescription = null!;
|
||||
|
||||
private OsuTextBox usernameTextBox;
|
||||
private OsuTextBox emailTextBox;
|
||||
private OsuPasswordTextBox passwordTextBox;
|
||||
private OsuTextBox usernameTextBox = null!;
|
||||
private OsuTextBox emailTextBox = null!;
|
||||
private OsuPasswordTextBox passwordTextBox = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
private ShakeContainer registerShake;
|
||||
private ITextPart characterCheckText;
|
||||
private IBindable<APIState> apiState = null!;
|
||||
|
||||
private OsuTextBox[] textboxes;
|
||||
private LoadingLayer loadingLayer;
|
||||
private ShakeContainer registerShake = null!;
|
||||
private ITextPart characterCheckText = null!;
|
||||
|
||||
private OsuTextBox[] textboxes = null!;
|
||||
private LoadingLayer loadingLayer = null!;
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
private GameHost? host { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuGame game { get; set; }
|
||||
private OsuGame? game { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@ -144,6 +145,8 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
|
||||
passwordTextBox.Current.BindValueChanged(_ => updateCharacterCheckTextColour(), true);
|
||||
characterCheckText.DrawablePartsRecreated += _ => updateCharacterCheckTextColour();
|
||||
|
||||
apiState = api.State.GetBoundCopy();
|
||||
}
|
||||
|
||||
private void updateCharacterCheckTextColour()
|
||||
@ -180,7 +183,7 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
Task.Run(() =>
|
||||
{
|
||||
bool success;
|
||||
RegistrationRequest.RegistrationRequestErrors errors = null;
|
||||
RegistrationRequest.RegistrationRequestErrors? errors = null;
|
||||
|
||||
try
|
||||
{
|
||||
@ -210,7 +213,7 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
if (!string.IsNullOrEmpty(errors.Message))
|
||||
passwordDescription.AddErrors(new[] { errors.Message });
|
||||
|
||||
game.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", true);
|
||||
game?.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", true);
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -223,6 +226,12 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
return;
|
||||
}
|
||||
|
||||
apiState.BindValueChanged(state =>
|
||||
{
|
||||
if (state.NewValue == APIState.RequiresSecondFactorAuth)
|
||||
this.Push(new ScreenEmailVerification());
|
||||
});
|
||||
|
||||
api.Login(usernameTextBox.Text, passwordTextBox.Text);
|
||||
});
|
||||
});
|
||||
@ -241,6 +250,6 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
return false;
|
||||
}
|
||||
|
||||
private OsuTextBox nextUnfilledTextBox() => textboxes.FirstOrDefault(t => string.IsNullOrEmpty(t.Text));
|
||||
private OsuTextBox? nextUnfilledTextBox() => textboxes.FirstOrDefault(t => string.IsNullOrEmpty(t.Text));
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -23,14 +21,14 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
{
|
||||
public partial class ScreenWarning : AccountCreationScreen
|
||||
{
|
||||
private OsuTextFlowContainer multiAccountExplanationText;
|
||||
private LinkFlowContainer furtherAssistance;
|
||||
private OsuTextFlowContainer multiAccountExplanationText = null!;
|
||||
private LinkFlowContainer furtherAssistance = null!;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private IAPIProvider api { get; set; }
|
||||
[Resolved]
|
||||
private IAPIProvider? api { get; set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private OsuGame game { get; set; }
|
||||
[Resolved]
|
||||
private OsuGame? game { get; set; }
|
||||
|
||||
private const string help_centre_url = "/help/wiki/Help_Centre#login";
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@ -24,7 +23,9 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
private const float transition_time = 400;
|
||||
|
||||
private ScreenWelcome welcomeScreen;
|
||||
private ScreenWelcome welcomeScreen = null!;
|
||||
|
||||
private ScheduledDelegate? scheduledHide;
|
||||
|
||||
public AccountCreationOverlay()
|
||||
{
|
||||
@ -107,8 +108,6 @@ namespace osu.Game.Overlays
|
||||
this.FadeOut(100);
|
||||
}
|
||||
|
||||
private ScheduledDelegate scheduledHide;
|
||||
|
||||
private void apiStateChanged(ValueChangedEvent<APIState> state)
|
||||
{
|
||||
switch (state.NewValue)
|
||||
@ -118,12 +117,16 @@ namespace osu.Game.Overlays
|
||||
break;
|
||||
|
||||
case APIState.Connecting:
|
||||
case APIState.RequiresSecondFactorAuth:
|
||||
break;
|
||||
|
||||
case APIState.Online:
|
||||
scheduledHide?.Cancel();
|
||||
scheduledHide = Schedule(Hide);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Comments
|
||||
|
||||
public Color4 AccentColour { get; set; }
|
||||
|
||||
protected override IEnumerable<Drawable> EffectTargets => null;
|
||||
protected override IEnumerable<Drawable> EffectTargets => Enumerable.Empty<Drawable>();
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
@ -32,13 +32,7 @@ namespace osu.Game.Overlays.Login
|
||||
|
||||
public Action? RequestHide;
|
||||
|
||||
private void performLogin()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text))
|
||||
api.Login(username.Text, password.Text);
|
||||
else
|
||||
shakeSignIn.Shake();
|
||||
}
|
||||
public override bool AcceptsFocus => true;
|
||||
|
||||
[BackgroundDependencyLoader(permitNulls: true)]
|
||||
private void load(OsuConfigManager config, AccountCreationOverlay accountCreation)
|
||||
@ -144,7 +138,13 @@ namespace osu.Game.Overlays.Login
|
||||
}
|
||||
}
|
||||
|
||||
public override bool AcceptsFocus => true;
|
||||
private void performLogin()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text))
|
||||
api.Login(username.Text, password.Text);
|
||||
else
|
||||
shakeSignIn.Shake();
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e) => true;
|
||||
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Login
|
||||
{
|
||||
private bool bounding = true;
|
||||
|
||||
private LoginForm? form;
|
||||
private Drawable? form;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
@ -81,6 +81,10 @@ namespace osu.Game.Overlays.Login
|
||||
};
|
||||
break;
|
||||
|
||||
case APIState.RequiresSecondFactorAuth:
|
||||
Child = form = new SecondFactorAuthForm();
|
||||
break;
|
||||
|
||||
case APIState.Failing:
|
||||
case APIState.Connecting:
|
||||
LinkFlowContainer linkFlow;
|
||||
|
147
osu.Game/Overlays/Login/SecondFactorAuthForm.cs
Normal file
147
osu.Game/Overlays/Login/SecondFactorAuthForm.cs
Normal file
@ -0,0 +1,147 @@
|
||||
// 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.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Login
|
||||
{
|
||||
public partial class SecondFactorAuthForm : Container
|
||||
{
|
||||
private OsuTextBox codeTextBox = null!;
|
||||
private LinkFlowContainer explainText = null!;
|
||||
private ErrorTextFlowContainer errorText = null!;
|
||||
|
||||
private LoadingLayer loading = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, SettingsSection.ITEM_SPACING),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS },
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, SettingsSection.ITEM_SPACING),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular))
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Text = "An email has been sent to you with a verification code. Enter the code.",
|
||||
},
|
||||
codeTextBox = new OsuTextBox
|
||||
{
|
||||
PlaceholderText = "Enter code",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
explainText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular))
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
},
|
||||
errorText = new ErrorTextFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Alpha = 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
new LinkFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
},
|
||||
}
|
||||
},
|
||||
loading = new LoadingLayer(true)
|
||||
{
|
||||
Padding = new MarginPadding { Vertical = -SettingsSection.ITEM_SPACING },
|
||||
}
|
||||
};
|
||||
|
||||
explainText.AddParagraph(UserVerificationStrings.BoxInfoCheckSpam);
|
||||
// We can't support localisable strings with nested links yet. Not sure if we even can (probably need to allow markdown link formatting or something).
|
||||
explainText.AddParagraph("If you can't access your email or have forgotten what you used, please follow the ");
|
||||
explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.WebsiteRootUrl}/home/password-reset");
|
||||
explainText.AddText(". You can also ");
|
||||
explainText.AddLink(UserVerificationStrings.BoxInfoReissueLink, () =>
|
||||
{
|
||||
loading.Show();
|
||||
|
||||
var reissueRequest = new ReissueVerificationCodeRequest();
|
||||
reissueRequest.Failure += ex =>
|
||||
{
|
||||
Logger.Error(ex, @"Failed to retrieve new verification code.");
|
||||
loading.Hide();
|
||||
};
|
||||
reissueRequest.Success += () =>
|
||||
{
|
||||
loading.Hide();
|
||||
};
|
||||
|
||||
Task.Run(() => api.Perform(reissueRequest));
|
||||
});
|
||||
explainText.AddText(" or ");
|
||||
explainText.AddLink(UserVerificationStrings.BoxInfoLogoutLink, () => { api.Logout(); });
|
||||
explainText.AddText(".");
|
||||
|
||||
codeTextBox.Current.BindValueChanged(code =>
|
||||
{
|
||||
if (code.NewValue.Length == 8)
|
||||
{
|
||||
api.AuthenticateSecondFactor(code.NewValue);
|
||||
codeTextBox.Current.Disabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (api.LastLoginError?.Message is string error)
|
||||
{
|
||||
errorText.Alpha = 1;
|
||||
errorText.AddErrors(new[] { error });
|
||||
}
|
||||
}
|
||||
|
||||
public override bool AcceptsFocus => true;
|
||||
|
||||
protected override bool OnClick(ClickEvent e) => true;
|
||||
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
{
|
||||
Schedule(() => { GetContainingInputManager().ChangeFocus(codeTextBox); });
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user