mirror of
https://github.com/ppy/osu.git
synced 2025-02-21 03:02:54 +08:00
Merge branch 'master' into results-clean
This commit is contained in:
commit
7b2adc857a
@ -3,13 +3,13 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"jetbrains.resharper.globaltools": {
|
"jetbrains.resharper.globaltools": {
|
||||||
"version": "2022.2.3",
|
"version": "2023.3.3",
|
||||||
"commands": [
|
"commands": [
|
||||||
"jb"
|
"jb"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nvika": {
|
"nvika": {
|
||||||
"version": "2.2.0",
|
"version": "3.0.0",
|
||||||
"commands": [
|
"commands": [
|
||||||
"nvika"
|
"nvika"
|
||||||
]
|
]
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
is_global = true
|
|
||||||
|
|
||||||
# .NET Code Style
|
# .NET Code Style
|
||||||
# IDE styles reference: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/
|
# 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.
|
# Temporarily disable analysing CanBeNull = true in NRT contexts due to mobile issues.
|
||||||
# See: https://github.com/ppy/osu/pull/19677
|
# 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>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.127.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.131.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
@ -20,5 +20,6 @@
|
|||||||
<file src="**.dll" target="lib\net45\"/>
|
<file src="**.dll" target="lib\net45\"/>
|
||||||
<file src="**.config" target="lib\net45\"/>
|
<file src="**.config" target="lib\net45\"/>
|
||||||
<file src="**.json" target="lib\net45\"/>
|
<file src="**.json" target="lib\net45\"/>
|
||||||
|
<file src="icon.png" target=""/>
|
||||||
</files>
|
</files>
|
||||||
</package>
|
</package>
|
||||||
|
@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
{
|
{
|
||||||
foreach (var stage in stages)
|
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 };
|
var obj = new Note { Column = i, StartTime = Time.Current + 2000 };
|
||||||
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
{
|
{
|
||||||
foreach (var stage in stages)
|
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 };
|
var obj = new HoldNote { Column = i, StartTime = Time.Current + 2000, Duration = 500 };
|
||||||
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
|
@ -17,7 +17,7 @@ using osuTK.Graphics;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
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;
|
private const float judgement_y_position = 160;
|
||||||
|
|
||||||
|
@ -4,8 +4,6 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Framework;
|
using osu.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -28,20 +26,21 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// All contents added to this <see cref="ColumnFlow{TContent}"/>.
|
/// All contents added to this <see cref="ColumnFlow{TContent}"/>.
|
||||||
/// </summary>
|
/// </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;
|
private readonly StageDefinition stageDefinition;
|
||||||
|
|
||||||
public ColumnFlow(StageDefinition stageDefinition)
|
public ColumnFlow(StageDefinition stageDefinition)
|
||||||
{
|
{
|
||||||
this.stageDefinition = stageDefinition;
|
this.stageDefinition = stageDefinition;
|
||||||
|
Content = new TContent[stageDefinition.Columns];
|
||||||
|
|
||||||
AutoSizeAxes = Axes.X;
|
AutoSizeAxes = Axes.X;
|
||||||
|
|
||||||
Masking = true;
|
Masking = true;
|
||||||
|
|
||||||
InternalChild = columns = new FillFlowContainer<Container>
|
InternalChild = columns = new FillFlowContainer<Container<TContent>>
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Y,
|
RelativeSizeAxes = Axes.Y,
|
||||||
AutoSizeAxes = Axes.X,
|
AutoSizeAxes = Axes.X,
|
||||||
@ -49,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (int i = 0; i < stageDefinition.Columns; i++)
|
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;
|
private ISkinSource currentSkin;
|
||||||
@ -102,7 +101,10 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="column">The index of the column to set the content of.</param>
|
/// <param name="column">The index of the column to set the content of.</param>
|
||||||
/// <param name="content">The content.</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()
|
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)
|
public ManiaPlayfield(List<StageDefinition> stageDefinitions)
|
||||||
{
|
{
|
||||||
@ -71,7 +80,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
stages.Add(newStage);
|
stages.Add(newStage);
|
||||||
AddNested(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)
|
foreach (var stage in stages)
|
||||||
{
|
{
|
||||||
if (index >= stage.Columns.Count)
|
if (index >= stage.Columns.Length)
|
||||||
{
|
{
|
||||||
index -= stage.Columns.Count;
|
index -= stage.Columns.Length;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,7 +149,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the total amount of columns across all stages in this playfield.
|
/// Retrieves the total amount of columns across all stages in this playfield.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int TotalColumns => stages.Sum(s => s.Columns.Count);
|
public int TotalColumns => stages.Sum(s => s.Columns.Length);
|
||||||
|
|
||||||
private Stage getStageByColumn(int column)
|
private Stage getStageByColumn(int column)
|
||||||
{
|
{
|
||||||
@ -148,7 +157,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
|
|
||||||
foreach (var stage in stages)
|
foreach (var stage in stages)
|
||||||
{
|
{
|
||||||
sum += stage.Columns.Count;
|
sum += stage.Columns.Length;
|
||||||
if (sum > column)
|
if (sum > column)
|
||||||
return stage;
|
return stage;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
|
|
||||||
public const float HIT_TARGET_POSITION = 110;
|
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 ColumnFlow<Column> columnFlow;
|
||||||
|
|
||||||
private readonly JudgementContainer<DrawableManiaJudgement> judgements;
|
private readonly JudgementContainer<DrawableManiaJudgement> judgements;
|
||||||
@ -45,7 +44,16 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
|
|
||||||
private readonly Drawable barLineContainer;
|
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;
|
private readonly int firstColumnIndex;
|
||||||
|
|
||||||
@ -184,13 +192,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
NewResult += OnNewResult;
|
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);
|
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}\"", () =>
|
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();
|
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)
|
new PathControlPoint(new Vector2(-128, 0), PathType.LINEAR) // absolute position: (0, 128)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
RepeatCount = 1
|
RepeatCount = 2
|
||||||
};
|
};
|
||||||
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
return slider;
|
return slider;
|
||||||
@ -45,7 +45,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
OsuHitObjectGenerationUtils.ReflectHorizontallyAlongPlayfield(slider);
|
OsuHitObjectGenerationUtils.ReflectHorizontallyAlongPlayfield(slider);
|
||||||
|
|
||||||
Assert.That(slider.Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 128, 128)));
|
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[]
|
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
|
||||||
{
|
{
|
||||||
new Vector2(),
|
new Vector2(),
|
||||||
@ -62,7 +64,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(slider);
|
OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(slider);
|
||||||
|
|
||||||
Assert.That(slider.Position, Is.EqualTo(new Vector2(128, OsuPlayfield.BASE_SIZE.Y - 128)));
|
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[]
|
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
|
||||||
{
|
{
|
||||||
new Vector2(),
|
new Vector2(),
|
||||||
@ -79,7 +83,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
OsuHitObjectGenerationUtils.FlipSliderInPlaceHorizontally(slider);
|
OsuHitObjectGenerationUtils.FlipSliderInPlaceHorizontally(slider);
|
||||||
|
|
||||||
Assert.That(slider.Position, Is.EqualTo(new Vector2(128, 128)));
|
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[]
|
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
|
||||||
{
|
{
|
||||||
new Vector2(),
|
new Vector2(),
|
||||||
|
@ -183,7 +183,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
hitObjectContainer.Add(drawableObject);
|
hitObjectContainer.Add(drawableObject!);
|
||||||
followPointRenderer.AddFollowPoints(objects[i]);
|
followPointRenderer.AddFollowPoints(objects[i]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
@ -173,6 +174,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
public IEnumerable<ISkin> AllSources => new[] { this };
|
public IEnumerable<ISkin> AllSources => new[] { this };
|
||||||
|
|
||||||
|
[CanBeNull]
|
||||||
public event Action SourceChanged;
|
public event Action SourceChanged;
|
||||||
|
|
||||||
private bool enabled = true;
|
private bool enabled = true;
|
||||||
|
@ -239,11 +239,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
if (Tracking.Value && Time.Current >= HitObject.StartTime)
|
if (Tracking.Value && Time.Current >= HitObject.StartTime)
|
||||||
{
|
{
|
||||||
// keep the sliding sample playing at the current tracking position
|
// keep the sliding sample playing at the current tracking position
|
||||||
if (!slidingSample.IsPlaying)
|
if (!slidingSample.RequestedPlaying)
|
||||||
slidingSample.Play();
|
slidingSample.Play();
|
||||||
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball));
|
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball));
|
||||||
}
|
}
|
||||||
else if (slidingSample.IsPlaying)
|
else if (slidingSample.IsPlaying || slidingSample.RequestedPlaying)
|
||||||
slidingSample.Stop();
|
slidingSample.Stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
{
|
{
|
||||||
public const double ANIM_DURATION = 150;
|
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;
|
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
|
||||||
|
|
||||||
@ -44,8 +44,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
{
|
{
|
||||||
Masking = true,
|
Masking = true,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Size = new Vector2(default_tick_size),
|
Size = new Vector2(DEFAULT_TICK_SIZE),
|
||||||
BorderThickness = default_tick_size / 4,
|
BorderThickness = DEFAULT_TICK_SIZE / 4,
|
||||||
BorderColour = Color4.White,
|
BorderColour = Color4.White,
|
||||||
Child = new Box
|
Child = new Box
|
||||||
{
|
{
|
||||||
@ -88,8 +88,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ArmedState.Miss:
|
case ArmedState.Miss:
|
||||||
this.FadeOut(ANIM_DURATION);
|
this.FadeOut(ANIM_DURATION, Easing.OutQuint);
|
||||||
this.TransformBindableTo(AccentColour, Color4.Red, 0);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ArmedState.Hit:
|
case ArmedState.Hit:
|
||||||
|
@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
{
|
{
|
||||||
if (tracking.NewValue)
|
if (tracking.NewValue)
|
||||||
{
|
{
|
||||||
if (!spinningSample.IsPlaying)
|
if (!spinningSample.RequestedPlaying)
|
||||||
spinningSample.Play();
|
spinningSample.Play();
|
||||||
|
|
||||||
spinningSample.VolumeTo(1, 300);
|
spinningSample.VolumeTo(1, 300);
|
||||||
|
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
set
|
set
|
||||||
{
|
{
|
||||||
repeatCount = value;
|
repeatCount = value;
|
||||||
endPositionCache.Invalidate();
|
updateNestedPositions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
public Slider()
|
public Slider()
|
||||||
{
|
{
|
||||||
SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples();
|
SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples();
|
||||||
Path.Version.ValueChanged += _ => endPositionCache.Invalidate();
|
Path.Version.ValueChanged += _ => updateNestedPositions();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||||
|
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
base.ReloadMappings(realmKeyBindings);
|
base.ReloadMappings(realmKeyBindings);
|
||||||
|
|
||||||
if (!AllowGameplayInputs)
|
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.Replays;
|
||||||
using osu.Game.Rulesets.Osu.Scoring;
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
using osu.Game.Rulesets.Osu.Skinning.Argon;
|
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.Skinning.Legacy;
|
||||||
using osu.Game.Rulesets.Osu.Statistics;
|
using osu.Game.Rulesets.Osu.Statistics;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
@ -254,6 +255,9 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
|
|
||||||
case ArgonSkin:
|
case ArgonSkin:
|
||||||
return new OsuArgonSkinTransformer(skin);
|
return new OsuArgonSkinTransformer(skin);
|
||||||
|
|
||||||
|
case TrianglesSkin:
|
||||||
|
return new OsuTrianglesSkinTransformer(skin);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -16,7 +16,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||||
{
|
{
|
||||||
public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
|
public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement
|
||||||
{
|
{
|
||||||
private RingExplosion? ringExplosion;
|
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 Bindable<Color4> accentColour = null!;
|
||||||
|
|
||||||
private const float size = 12;
|
public const float SIZE = 12;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(DrawableHitObject hitObject)
|
private void load(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
Masking = true;
|
Masking = true;
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
Size = new Vector2(size);
|
Size = new Vector2(SIZE);
|
||||||
BorderThickness = 3;
|
BorderThickness = 3;
|
||||||
BorderColour = Color4.White;
|
BorderColour = Color4.White;
|
||||||
Child = new Box
|
Child = new Box
|
||||||
|
@ -19,11 +19,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
switch (lookup)
|
switch (lookup)
|
||||||
{
|
{
|
||||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
||||||
|
HitResult result = resultComponent.Component;
|
||||||
|
|
||||||
// This should eventually be moved to a skin setting, when supported.
|
// 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 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:
|
case OsuSkinComponentLookup osuComponent:
|
||||||
// TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries.
|
// 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
|
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||||
{
|
{
|
||||||
public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
|
public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement
|
||||||
{
|
{
|
||||||
private RingExplosion? ringExplosion;
|
private RingExplosion? ringExplosion;
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ using osu.Game.Replays;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
using osu.Game.Rulesets.Taiko.Replays;
|
using osu.Game.Rulesets.Taiko.Replays;
|
||||||
using osu.Game.Rulesets.Timing;
|
using osu.Game.Rulesets.Timing;
|
||||||
@ -36,6 +35,8 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
|
|
||||||
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
|
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
|
||||||
|
|
||||||
|
protected new TaikoPlayfieldAdjustmentContainer PlayfieldAdjustmentContainer => (TaikoPlayfieldAdjustmentContainer)base.PlayfieldAdjustmentContainer;
|
||||||
|
|
||||||
protected override bool UserScrollSpeedAdjustment => false;
|
protected override bool UserScrollSpeedAdjustment => false;
|
||||||
|
|
||||||
private SkinnableDrawable scroller;
|
private SkinnableDrawable scroller;
|
||||||
@ -68,22 +69,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
TimeRange.Value = ComputeTimeRange();
|
TimeRange.Value = ComputeTimeRange();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual double ComputeTimeRange()
|
protected virtual double ComputeTimeRange() => PlayfieldAdjustmentContainer.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 override void UpdateAfterChildren()
|
protected override void UpdateAfterChildren()
|
||||||
{
|
{
|
||||||
|
@ -179,10 +179,9 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
TaikoAction taikoAction = getTaikoActionFromPosition(position);
|
TaikoAction taikoAction = getTaikoActionFromPosition(position);
|
||||||
|
|
||||||
// Not too sure how this can happen, but let's avoid throwing.
|
// Not too sure how this can happen, but let's avoid throwing.
|
||||||
if (trackedActions.ContainsKey(source))
|
if (!trackedActions.TryAdd(source, taikoAction))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
trackedActions.Add(source, taikoAction);
|
|
||||||
keyBindingContainer.TriggerPressed(taikoAction);
|
keyBindingContainer.TriggerPressed(taikoAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -14,6 +15,8 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
public const float MAXIMUM_ASPECT = 16f / 9f;
|
public const float MAXIMUM_ASPECT = 16f / 9f;
|
||||||
public const float MINIMUM_ASPECT = 5f / 4f;
|
public const float MINIMUM_ASPECT = 5f / 4f;
|
||||||
|
|
||||||
|
private const float stable_gamefield_height = 480f;
|
||||||
|
|
||||||
public readonly IBindable<bool> LockPlayfieldAspectRange = new BindableBool(true);
|
public readonly IBindable<bool> LockPlayfieldAspectRange = new BindableBool(true);
|
||||||
|
|
||||||
public TaikoPlayfieldAdjustmentContainer()
|
public TaikoPlayfieldAdjustmentContainer()
|
||||||
@ -21,6 +24,9 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
RelativePositionAxes = Axes.Y;
|
RelativePositionAxes = Axes.Y;
|
||||||
Height = TaikoPlayfield.BASE_HEIGHT;
|
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()
|
protected override void Update()
|
||||||
@ -28,8 +34,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
const float base_relative_height = TaikoPlayfield.BASE_HEIGHT / 768;
|
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;
|
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.
|
// 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);
|
relativeHeight = Math.Min(relativeHeight, 1f / 3f);
|
||||||
|
|
||||||
Y = base_position;
|
|
||||||
|
|
||||||
Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f));
|
Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f));
|
||||||
Width = 1 / Scale.X;
|
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.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -98,9 +99,10 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
Beatmap = beatmap;
|
Beatmap = beatmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma warning disable CS0067
|
||||||
|
[CanBeNull]
|
||||||
public event Action<HitObject, IEnumerable<HitObject>> ObjectConverted;
|
public event Action<HitObject, IEnumerable<HitObject>> ObjectConverted;
|
||||||
|
#pragma warning restore CS0067
|
||||||
protected virtual void OnObjectConverted(HitObject arg1, IEnumerable<HitObject> arg2) => ObjectConverted?.Invoke(arg1, arg2);
|
|
||||||
|
|
||||||
public IBeatmap Beatmap { get; }
|
public IBeatmap Beatmap { get; }
|
||||||
|
|
||||||
|
@ -75,8 +75,6 @@ namespace osu.Game.Tests.Chat
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for notifications client", () => channelManager.NotificationsConnected);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -79,7 +79,7 @@ namespace osu.Game.Tests.Database
|
|||||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
|
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
|
||||||
|
|
||||||
// Availability is updated on construction of a RealmRulesetStore
|
// 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);
|
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);
|
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
|
||||||
|
|
||||||
// Availability is updated on construction of a RealmRulesetStore
|
// 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);
|
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.False);
|
||||||
|
|
||||||
// Simulate the ruleset getting updated
|
// Simulate the ruleset getting updated
|
||||||
LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION;
|
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);
|
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 IRenderer Renderer => host.Renderer;
|
||||||
public AudioManager AudioManager => Audio;
|
public AudioManager AudioManager => Audio;
|
||||||
public IResourceStore<byte[]> Files => null;
|
public IResourceStore<byte[]> Files => null!;
|
||||||
public new IResourceStore<byte[]> Resources => base.Resources;
|
public new IResourceStore<byte[]> Resources => base.Resources;
|
||||||
public RealmAccess RealmAccess => null;
|
public RealmAccess RealmAccess => null!;
|
||||||
public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => null;
|
public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => null;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -169,9 +169,9 @@ namespace osu.Game.Tests.NonVisual.Skinning
|
|||||||
|
|
||||||
public IRenderer Renderer => new DummyRenderer();
|
public IRenderer Renderer => new DummyRenderer();
|
||||||
public AudioManager AudioManager => null;
|
public AudioManager AudioManager => null;
|
||||||
public IResourceStore<byte[]> Files => null;
|
public IResourceStore<byte[]> Files => null!;
|
||||||
public IResourceStore<byte[]> Resources => null;
|
public IResourceStore<byte[]> Resources => null!;
|
||||||
public RealmAccess RealmAccess => null;
|
public RealmAccess RealmAccess => null!;
|
||||||
public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => textureStore;
|
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 IEnumerable<Mod> GetModsFor(ModType type) => new Mod[] { null };
|
||||||
|
|
||||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => null;
|
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => null!;
|
||||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null;
|
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!;
|
||||||
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null;
|
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestAPIIncompatibleRuleset : Ruleset
|
private class TestAPIIncompatibleRuleset : Ruleset
|
||||||
@ -69,9 +69,9 @@ namespace osu.Game.Tests.Rulesets
|
|||||||
// simulate API incompatibility by throwing similar exceptions.
|
// simulate API incompatibility by throwing similar exceptions.
|
||||||
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new MissingMethodException();
|
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new MissingMethodException();
|
||||||
|
|
||||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => null;
|
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => null!;
|
||||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null;
|
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!;
|
||||||
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null;
|
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,7 +142,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
AddStep("dismiss prompt", () =>
|
AddStep("dismiss prompt", () =>
|
||||||
{
|
{
|
||||||
var button = DialogOverlay.CurrentDialog.Buttons.Last();
|
var button = DialogOverlay.CurrentDialog!.Buttons.Last();
|
||||||
InputManager.MoveMouseTo(button);
|
InputManager.MoveMouseTo(button);
|
||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
});
|
});
|
||||||
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
|
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
|
||||||
|
|
||||||
AddStep("save changes", () => DialogOverlay.CurrentDialog.PerformOkAction());
|
AddStep("save changes", () => DialogOverlay.CurrentDialog!.PerformOkAction());
|
||||||
|
|
||||||
EditorPlayer editorPlayer = null;
|
EditorPlayer editorPlayer = null;
|
||||||
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as 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());
|
seekTo(referenceBeatmap.HitObjects[^1].GetEndTime());
|
||||||
AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true);
|
AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true);
|
||||||
|
|
||||||
AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100);
|
AddAssert("score has combo", () => getResultsScreen().Score!.Combo > 100);
|
||||||
AddAssert("score has no misses", () => getResultsScreen().Score.Statistics[HitResult.Miss] == 0);
|
AddAssert("score has no misses", () => getResultsScreen().Score!.Statistics[HitResult.Miss] == 0);
|
||||||
|
|
||||||
AddUntilStep("avatar displayed", () => getAvatar() != null);
|
AddUntilStep("avatar displayed", () => getAvatar() != null);
|
||||||
AddAssert("avatar not clickable", () => getAvatar().ChildrenOfType<OsuClickableContainer>().First().Action == 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);
|
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.
|
// 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 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 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("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()));
|
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();
|
CreateTest();
|
||||||
|
|
||||||
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
|
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()));
|
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()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
playbackManager?.ReplayInputHandler.SetFrameFromTime(Time.Current - 100);
|
playbackManager?.ReplayInputHandler?.SetFrameFromTime(Time.Current - 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TearDownSteps]
|
[TearDownSteps]
|
||||||
|
@ -198,7 +198,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
foreach (var legacyFrame in frames.Frames)
|
foreach (var legacyFrame in frames.Frames)
|
||||||
{
|
{
|
||||||
var frame = new TestReplayFrame();
|
var frame = new TestReplayFrame();
|
||||||
frame.FromLegacy(legacyFrame, null);
|
frame.FromLegacy(legacyFrame, null!);
|
||||||
playbackReplay.Frames.Add(frame);
|
playbackReplay.Frames.Add(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -9,7 +10,9 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.API.Requests;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Login;
|
||||||
using osu.Game.Users.Drawables;
|
using osu.Game.Users.Drawables;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
@ -18,6 +21,8 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public partial class TestSceneLoginOverlay : OsuManualInputManagerTestScene
|
public partial class TestSceneLoginOverlay : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
|
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||||
|
|
||||||
private LoginOverlay loginOverlay = null!;
|
private LoginOverlay loginOverlay = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -40,9 +45,69 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
public void TestLoginSuccess()
|
public void TestLoginSuccess()
|
||||||
{
|
{
|
||||||
AddStep("logout", () => API.Logout());
|
AddStep("logout", () => API.Logout());
|
||||||
|
assertAPIState(APIState.Offline);
|
||||||
|
|
||||||
AddStep("enter password", () => loginOverlay.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
|
AddStep("enter password", () => loginOverlay.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
|
||||||
AddStep("submit", () => loginOverlay.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
|
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]
|
[Test]
|
||||||
@ -78,6 +143,12 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
AddStep("enter password", () => loginOverlay.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
|
AddStep("enter password", () => loginOverlay.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
|
||||||
AddStep("submit", () => loginOverlay.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
|
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", () =>
|
AddStep("click on flag", () =>
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<UpdateableFlag>().First());
|
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>();
|
public IEnumerable<Notification> AllNotifications => Enumerable.Empty<Notification>();
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public partial class TestSceneToolbarUserButton : OsuManualInputManagerTestScene
|
public partial class TestSceneToolbarUserButton : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
|
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||||
|
|
||||||
public TestSceneToolbarUserButton()
|
public TestSceneToolbarUserButton()
|
||||||
{
|
{
|
||||||
Container mainContainer;
|
Container mainContainer;
|
||||||
@ -69,18 +71,20 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestLoginLogout()
|
public void TestLoginLogout()
|
||||||
{
|
{
|
||||||
AddStep("Log out", () => ((DummyAPIAccess)API).Logout());
|
AddStep("Log out", () => dummyAPI.Logout());
|
||||||
AddStep("Log in", () => ((DummyAPIAccess)API).Login("wang", "jang"));
|
AddStep("Log in", () => dummyAPI.Login("wang", "jang"));
|
||||||
|
AddStep("Authenticate via second factor", () => dummyAPI.AuthenticateSecondFactor("abcdefgh"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestStates()
|
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>())
|
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)
|
.SkipWhile(r => r.Room.Category.Value == RoomCategory.Spotlight)
|
||||||
.All(r => r.Room.Category.Value == RoomCategory.Normal));
|
.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("has 4 rooms", () => container.Rooms.Count == 4);
|
||||||
AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0));
|
AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0));
|
||||||
|
|
||||||
|
@ -698,7 +698,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
var scoreInfo = ((ResultsScreen)multiplayerComponents.CurrentScreen).Score;
|
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
|
#nullable disable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu;
|
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.OnlinePlay.Multiplayer;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
@ -19,14 +24,37 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
private MultiplayerPlayer player;
|
private MultiplayerPlayer player;
|
||||||
|
|
||||||
[SetUpSteps]
|
[Test]
|
||||||
public override void SetUpSteps()
|
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", () =>
|
AddStep("set beatmap", () =>
|
||||||
{
|
{
|
||||||
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||||
|
SelectedMods.Value = mods?.Invoke() ?? Array.Empty<Mod>();
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("Start track playing", () =>
|
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);
|
AddUntilStep("gameplay clock is not paused", () => !player.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value);
|
||||||
AddAssert("gameplay clock is running", () => player.ChildrenOfType<GameplayClockContainer>().Single().IsRunning);
|
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:
|
case ScorePresentType.Results:
|
||||||
AddUntilStep("wait for results", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ResultsScreen);
|
AddUntilStep("wait for results", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ResultsScreen);
|
||||||
AddStep("store last waited screen", () => lastWaitedScreen = Game.ScreenStack.CurrentScreen);
|
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));
|
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.Equals(getImport().Ruleset));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.AccountCreation;
|
using osu.Game.Overlays.AccountCreation;
|
||||||
@ -59,7 +61,40 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddStep("click button", () => accountCreation.ChildrenOfType<SettingsButton>().Single().TriggerClick());
|
AddStep("click button", () => accountCreation.ChildrenOfType<SettingsButton>().Single().TriggerClick());
|
||||||
AddUntilStep("warning screen is present", () => accountCreation.ChildrenOfType<ScreenWarning>().SingleOrDefault()?.IsPresent == true);
|
AddUntilStep("warning screen is present", () => accountCreation.ChildrenOfType<ScreenWarning>().SingleOrDefault()?.IsPresent == true);
|
||||||
|
|
||||||
AddStep("log back in", () => API.Login("dummy", "password"));
|
AddStep("log back in", () =>
|
||||||
|
{
|
||||||
|
API.Login("dummy", "password");
|
||||||
|
((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);
|
AddUntilStep("overlay is hidden", () => accountCreation.State.Value == Visibility.Hidden);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,6 +67,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Schedule(() =>
|
Schedule(() =>
|
||||||
{
|
{
|
||||||
API.Login("test", "test");
|
API.Login("test", "test");
|
||||||
|
dummyAPI.AuthenticateSecondFactor("abcdefgh");
|
||||||
Child = commentsContainer = new CommentsContainer();
|
Child = commentsContainer = new CommentsContainer();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays.BeatmapSet.Buttons;
|
using osu.Game.Overlays.BeatmapSet.Buttons;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -34,14 +35,22 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddStep("set valid beatmap", () => favourite.BeatmapSet.Value = new APIBeatmapSet { OnlineID = 88 });
|
AddStep("set valid beatmap", () => favourite.BeatmapSet.Value = new APIBeatmapSet { OnlineID = 88 });
|
||||||
AddStep("log out", () => API.Logout());
|
AddStep("log out", () => API.Logout());
|
||||||
checkEnabled(false);
|
checkEnabled(false);
|
||||||
AddStep("log in", () => API.Login("test", "test"));
|
AddStep("log in", () =>
|
||||||
|
{
|
||||||
|
API.Login("test", "test");
|
||||||
|
((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
|
||||||
|
});
|
||||||
checkEnabled(true);
|
checkEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestBeatmapChange()
|
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 });
|
AddStep("set valid beatmap", () => favourite.BeatmapSet.Value = new APIBeatmapSet { OnlineID = 88 });
|
||||||
checkEnabled(true);
|
checkEnabled(true);
|
||||||
AddStep("set invalid beatmap", () => favourite.BeatmapSet.Value = new APIBeatmapSet());
|
AddStep("set invalid beatmap", () => favourite.BeatmapSet.Value = new APIBeatmapSet());
|
||||||
|
@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
int userId = int.Parse(getUserRequest.Lookup);
|
int userId = int.Parse(getUserRequest.Lookup);
|
||||||
string rulesetName = getUserRequest.Ruleset.ShortName;
|
string rulesetName = getUserRequest.Ruleset!.ShortName;
|
||||||
var response = new APIUser
|
var response = new APIUser
|
||||||
{
|
{
|
||||||
Id = userId,
|
Id = userId,
|
||||||
@ -177,7 +177,11 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddWaitStep("wait a bit", 5);
|
AddWaitStep("wait a bit", 5);
|
||||||
AddAssert("update not received", () => update == null);
|
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]
|
[Test]
|
||||||
|
@ -52,7 +52,11 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
|
AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
|
||||||
AddToggleStep("toggle visibility", visible => profile.State.Value = visible ? Visibility.Visible : Visibility.Hidden);
|
AddToggleStep("toggle visibility", visible => profile.State.Value = visible ? Visibility.Visible : Visibility.Hidden);
|
||||||
AddStep("log out", () => dummyAPI.Logout());
|
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]
|
[Test]
|
||||||
@ -98,7 +102,11 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
});
|
});
|
||||||
AddStep("logout", () => dummyAPI.Logout());
|
AddStep("logout", () => dummyAPI.Logout());
|
||||||
AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
|
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);
|
AddWaitStep("wait some", 3);
|
||||||
AddStep("complete request", () => pendingRequest.TriggerSuccess(TEST_USER));
|
AddStep("complete request", () => pendingRequest.TriggerSuccess(TEST_USER));
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
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);
|
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
|
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 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();
|
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 LoadingSpinner RightSpinner => base.RightSpinner;
|
||||||
public new ScorePanelList ScorePanelList => base.ScorePanelList;
|
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)
|
: base(score, roomId, playlistItem, allowRetry)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -52,11 +52,11 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetupSteps()
|
public void SetupSteps()
|
||||||
{
|
{
|
||||||
AddStep("set room", () => SelectedRoom.Value = new Room());
|
AddStep("set room", () => SelectedRoom!.Value = new Room());
|
||||||
|
|
||||||
importBeatmap();
|
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());
|
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]
|
[Test]
|
||||||
@ -201,7 +201,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
|
|
||||||
private void setupAndCreateRoom(Action<Room> room)
|
private void setupAndCreateRoom(Action<Room> room)
|
||||||
{
|
{
|
||||||
AddStep("setup room", () => room(SelectedRoom.Value));
|
AddStep("setup room", () => room(SelectedRoom!.Value));
|
||||||
|
|
||||||
AddStep("click create button", () =>
|
AddStep("click create button", () =>
|
||||||
{
|
{
|
||||||
|
@ -418,7 +418,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
public UnrankedSoloResultsScreen(ScoreInfo score)
|
public UnrankedSoloResultsScreen(ScoreInfo score)
|
||||||
: base(score, true)
|
: base(score, true)
|
||||||
{
|
{
|
||||||
Score.BeatmapInfo!.OnlineID = 0;
|
Score!.BeatmapInfo!.OnlineID = 0;
|
||||||
Score.BeatmapInfo.Status = BeatmapOnlineStatus.Pending;
|
Score.BeatmapInfo.Status = BeatmapOnlineStatus.Pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -432,7 +432,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
|
|
||||||
private class RulesetWithNoPerformanceCalculator : OsuRuleset
|
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();
|
assertLoggedOutState();
|
||||||
|
|
||||||
// moving from logged out -> logged in
|
// 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();
|
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>();
|
public IEnumerable<Notification> AllNotifications => Enumerable.Empty<Notification>();
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,6 @@ namespace osu.Game.Beatmaps.ControlPoints
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly BindableDouble ScrollSpeedBindable = new BindableDouble(1)
|
public readonly BindableDouble ScrollSpeedBindable = new BindableDouble(1)
|
||||||
{
|
{
|
||||||
Precision = 0.01,
|
|
||||||
MinValue = 0.01,
|
MinValue = 0.01,
|
||||||
MaxValue = 10
|
MaxValue = 10
|
||||||
};
|
};
|
||||||
|
@ -567,10 +567,9 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
for (int i = pendingControlPoints.Count - 1; i >= 0; i--)
|
for (int i = pendingControlPoints.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
var type = pendingControlPoints[i].GetType();
|
var type = pendingControlPoints[i].GetType();
|
||||||
if (pendingControlPointTypes.Contains(type))
|
if (!pendingControlPointTypes.Add(type))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
pendingControlPointTypes.Add(type);
|
|
||||||
beatmap.ControlPointInfo.Add(pendingControlPointsTime, pendingControlPoints[i]);
|
beatmap.ControlPointInfo.Add(pendingControlPointsTime, pendingControlPoints[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ namespace osu.Game.Beatmaps
|
|||||||
ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
|
ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
|
||||||
IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer();
|
IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer();
|
||||||
AudioManager IStorageResourceProvider.AudioManager => audioManager;
|
AudioManager IStorageResourceProvider.AudioManager => audioManager;
|
||||||
RealmAccess IStorageResourceProvider.RealmAccess => null;
|
RealmAccess IStorageResourceProvider.RealmAccess => null!;
|
||||||
IResourceStore<byte[]> IStorageResourceProvider.Files => files;
|
IResourceStore<byte[]> IStorageResourceProvider.Files => files;
|
||||||
IResourceStore<byte[]> IStorageResourceProvider.Resources => resources;
|
IResourceStore<byte[]> IStorageResourceProvider.Resources => resources;
|
||||||
IResourceStore<TextureUpload> IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore);
|
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)
|
public override void Add(T drawable)
|
||||||
{
|
{
|
||||||
base.Add(drawable);
|
|
||||||
|
|
||||||
Debug.Assert(drawable != null);
|
Debug.Assert(drawable != null);
|
||||||
|
|
||||||
|
base.Add(drawable);
|
||||||
|
|
||||||
drawable.StateChanged += state => selectionChanged(drawable, state);
|
drawable.StateChanged += state => selectionChanged(drawable, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ namespace osu.Game.Graphics
|
|||||||
{
|
{
|
||||||
case HitResult.IgnoreMiss:
|
case HitResult.IgnoreMiss:
|
||||||
case HitResult.SmallTickMiss:
|
case HitResult.SmallTickMiss:
|
||||||
return Orange1;
|
return Color4.Gray;
|
||||||
|
|
||||||
case HitResult.Miss:
|
case HitResult.Miss:
|
||||||
case HitResult.LargeTickMiss:
|
case HitResult.LargeTickMiss:
|
||||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterface
|
namespace osu.Game.Graphics.UserInterface
|
||||||
@ -48,6 +49,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
{
|
{
|
||||||
protected virtual float ChevronSize => 10;
|
protected virtual float ChevronSize => 10;
|
||||||
|
|
||||||
|
[CanBeNull]
|
||||||
public event Action<Visibility> StateChanged;
|
public event Action<Visibility> StateChanged;
|
||||||
|
|
||||||
public readonly SpriteIcon Chevron;
|
public readonly SpriteIcon Chevron;
|
||||||
|
@ -41,6 +41,6 @@ namespace osu.Game.IO
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="underlyingStore">The underlying provider of texture data (in arbitrary image formats).</param>
|
/// <param name="underlyingStore">The underlying provider of texture data (in arbitrary image formats).</param>
|
||||||
/// <returns>A texture loader store.</returns>
|
/// <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>
|
/// </summary>
|
||||||
public static LocalisableString Connecting => new TranslatableString(getKey(@"connecting"), @"Connecting...");
|
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>
|
/// <summary>
|
||||||
/// "home"
|
/// "home"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -21,7 +21,7 @@ using osu.Game.Configuration;
|
|||||||
using osu.Game.Localisation;
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Online.API.Requests;
|
using osu.Game.Online.API.Requests;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
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.Online.Notifications.WebSocket;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
|
||||||
@ -48,6 +48,8 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
public string ProvidedUsername { get; private set; }
|
public string ProvidedUsername { get; private set; }
|
||||||
|
|
||||||
|
public string SecondFactorCode { get; private set; }
|
||||||
|
|
||||||
private string password;
|
private string password;
|
||||||
|
|
||||||
public IBindable<APIUser> LocalUser => localUser;
|
public IBindable<APIUser> LocalUser => localUser;
|
||||||
@ -55,6 +57,8 @@ namespace osu.Game.Online.API
|
|||||||
public IBindable<UserActivity> Activity => activity;
|
public IBindable<UserActivity> Activity => activity;
|
||||||
public IBindable<UserStatistics> Statistics => statistics;
|
public IBindable<UserStatistics> Statistics => statistics;
|
||||||
|
|
||||||
|
public INotificationsClient NotificationsClient { get; }
|
||||||
|
|
||||||
public Language Language => game.CurrentLanguage.Value;
|
public Language Language => game.CurrentLanguage.Value;
|
||||||
|
|
||||||
private Bindable<APIUser> localUser { get; } = new Bindable<APIUser>(createGuestUser());
|
private Bindable<APIUser> localUser { get; } = new Bindable<APIUser>(createGuestUser());
|
||||||
@ -82,6 +86,7 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
APIEndpointUrl = endpointConfiguration.APIEndpointUrl;
|
APIEndpointUrl = endpointConfiguration.APIEndpointUrl;
|
||||||
WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl;
|
WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl;
|
||||||
|
NotificationsClient = setUpNotificationsClient();
|
||||||
|
|
||||||
authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl);
|
authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl);
|
||||||
log = Logger.GetLogger(LoggingTarget.Network);
|
log = Logger.GetLogger(LoggingTarget.Network);
|
||||||
@ -114,6 +119,30 @@ namespace osu.Game.Online.API
|
|||||||
thread.Start();
|
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);
|
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);
|
internal new void Schedule(Action action) => base.Schedule(action);
|
||||||
@ -197,6 +226,7 @@ namespace osu.Game.Online.API
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This method takes control of <see cref="state"/> and transitions from <see cref="APIState.Connecting"/> to either
|
/// 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.Online"/> (successful connection)
|
||||||
/// - <see cref="APIState.Failing"/> (failed connection but retrying)
|
/// - <see cref="APIState.Failing"/> (failed connection but retrying)
|
||||||
/// - <see cref="APIState.Offline"/> (failed and can't retry, clear credentials and require user interaction)
|
/// - <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>
|
/// <returns>Whether the connection attempt was successful.</returns>
|
||||||
private void attemptConnect()
|
private void attemptConnect()
|
||||||
{
|
{
|
||||||
state.Value = APIState.Connecting;
|
|
||||||
|
|
||||||
if (localUser.IsDefault)
|
if (localUser.IsDefault)
|
||||||
{
|
{
|
||||||
// Show a placeholder user if saved credentials are available.
|
// Show a placeholder user if saved credentials are available.
|
||||||
@ -223,6 +251,7 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
if (!authentication.HasValidAccessToken)
|
if (!authentication.HasValidAccessToken)
|
||||||
{
|
{
|
||||||
|
state.Value = APIState.Connecting;
|
||||||
LastLoginError = null;
|
LastLoginError = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -240,40 +269,79 @@ namespace osu.Game.Online.API
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var userReq = new GetUserRequest();
|
switch (state.Value)
|
||||||
userReq.Failure += ex =>
|
|
||||||
{
|
{
|
||||||
if (ex is APIException)
|
case APIState.RequiresSecondFactorAuth:
|
||||||
{
|
{
|
||||||
LastLoginError = ex;
|
if (string.IsNullOrEmpty(SecondFactorCode))
|
||||||
log.Add($@"Login failed for username {ProvidedUsername} on user retrieval ({LastLoginError.Message})!");
|
return;
|
||||||
Logout();
|
|
||||||
|
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");
|
var userReq = new GetMeRequest();
|
||||||
Logout();
|
|
||||||
|
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();
|
var friendsReq = new GetFriendsRequest();
|
||||||
@ -321,11 +389,17 @@ namespace osu.Game.Online.API
|
|||||||
this.password = password;
|
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) =>
|
public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) =>
|
||||||
new HubClientConnector(clientName, endpoint, this, versionHash, preferMessagePack);
|
new HubClientConnector(clientName, endpoint, this, versionHash, preferMessagePack);
|
||||||
|
|
||||||
public NotificationsClientConnector GetNotificationsConnector() =>
|
public IChatClient GetChatClient() => new WebSocketChatClient(this);
|
||||||
new WebSocketNotificationsClientConnector(this);
|
|
||||||
|
|
||||||
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
|
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
|
||||||
{
|
{
|
||||||
@ -507,6 +581,7 @@ namespace osu.Game.Online.API
|
|||||||
public void Logout()
|
public void Logout()
|
||||||
{
|
{
|
||||||
password = null;
|
password = null;
|
||||||
|
SecondFactorCode = null;
|
||||||
authentication.Clear();
|
authentication.Clear();
|
||||||
|
|
||||||
// Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present
|
// 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>
|
/// </summary>
|
||||||
Failing,
|
Failing,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Waiting on second factor authentication.
|
||||||
|
/// </summary>
|
||||||
|
RequiresSecondFactorAuth,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// We are in the process of (re-)connecting.
|
/// We are in the process of (re-)connecting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -7,8 +7,10 @@ using System.Threading.Tasks;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Localisation;
|
using osu.Game.Localisation;
|
||||||
|
using osu.Game.Online.API.Requests;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
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.Tests;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
|
||||||
@ -30,6 +32,9 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
public Bindable<UserStatistics?> Statistics { get; } = new Bindable<UserStatistics?>();
|
public Bindable<UserStatistics?> Statistics { get; } = new Bindable<UserStatistics?>();
|
||||||
|
|
||||||
|
public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient();
|
||||||
|
INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient;
|
||||||
|
|
||||||
public Language Language => Language.en;
|
public Language Language => Language.en;
|
||||||
|
|
||||||
public string AccessToken => "token";
|
public string AccessToken => "token";
|
||||||
@ -57,6 +62,7 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
private bool shouldFailNextLogin;
|
private bool shouldFailNextLogin;
|
||||||
private bool stayConnectingNextLogin;
|
private bool stayConnectingNextLogin;
|
||||||
|
private bool requiredSecondFactorAuth = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current connectivity state of the API.
|
/// The current connectivity state of the API.
|
||||||
@ -117,13 +123,46 @@ namespace osu.Game.Online.API
|
|||||||
Id = DUMMY_USER_ID,
|
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
|
Statistics.Value = new UserStatistics
|
||||||
{
|
{
|
||||||
GlobalRank = 1,
|
GlobalRank = 1,
|
||||||
CountryRank = 1
|
CountryRank = 1
|
||||||
};
|
};
|
||||||
|
|
||||||
state.Value = APIState.Online;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Logout()
|
public void Logout()
|
||||||
@ -144,7 +183,7 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
public IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null;
|
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)
|
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<UserActivity> IAPIProvider.Activity => Activity;
|
||||||
IBindable<UserStatistics?> IAPIProvider.Statistics => Statistics;
|
IBindable<UserStatistics?> IAPIProvider.Statistics => Statistics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Skip 2FA requirement for next login.
|
||||||
|
/// </summary>
|
||||||
|
public void SkipSecondFactor() => requiredSecondFactorAuth = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// During the next simulated login, the process will fail immediately.
|
/// During the next simulated login, the process will fail immediately.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -6,7 +6,8 @@ using System.Threading.Tasks;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Localisation;
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
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;
|
using osu.Game.Users;
|
||||||
|
|
||||||
namespace osu.Game.Online.API
|
namespace osu.Game.Online.API
|
||||||
@ -111,6 +112,12 @@ namespace osu.Game.Online.API
|
|||||||
/// <param name="password">The user's password.</param>
|
/// <param name="password">The user's password.</param>
|
||||||
void Login(string username, string password);
|
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>
|
/// <summary>
|
||||||
/// Log out the current user.
|
/// Log out the current user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -130,9 +137,14 @@ namespace osu.Game.Online.API
|
|||||||
IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack = true);
|
IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack = true);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructs a new <see cref="NotificationsClientConnector"/>.
|
/// Accesses the <see cref="INotificationsClient"/> used to receive asynchronous notifications from web.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
NotificationsClientConnector GetNotificationsConnector();
|
INotificationsClient NotificationsClient { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a <see cref="IChatClient"/> instance to use in order to chat.
|
||||||
|
/// </summary>
|
||||||
|
IChatClient GetChatClient();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new user account. This is a blocking operation.
|
/// 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 we already have a valid access token, let's use it.
|
||||||
if (accessTokenValid) return true;
|
if (accessTokenValid) return true;
|
||||||
|
|
||||||
// we want to ensure only a single authentication update is happening at once.
|
// if not, let's try using our refresh token to request a new access token.
|
||||||
lock (access_token_retrieval_lock)
|
if (!string.IsNullOrEmpty(Token.Value?.RefreshToken))
|
||||||
{
|
// ReSharper disable once PossibleNullReferenceException
|
||||||
// re-check if valid, in case another request completed and revalidated our access.
|
AuthenticateWithRefresh(Token.Value.RefreshToken);
|
||||||
if (accessTokenValid) return true;
|
|
||||||
|
|
||||||
// if not, let's try using our refresh token to request a new access token.
|
return accessTokenValid;
|
||||||
if (!string.IsNullOrEmpty(Token.Value?.RefreshToken))
|
|
||||||
// ReSharper disable once PossibleNullReferenceException
|
|
||||||
AuthenticateWithRefresh(Token.Value.RefreshToken);
|
|
||||||
|
|
||||||
return accessTokenValid;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool accessTokenValid => Token.Value?.IsValid ?? false;
|
private bool accessTokenValid => Token.Value?.IsValid ?? false;
|
||||||
@ -149,14 +142,18 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
internal string RequestAccessToken()
|
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()
|
internal void Clear()
|
||||||
{
|
{
|
||||||
Token.Value = null;
|
lock (access_token_retrieval_lock)
|
||||||
|
Token.Value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class AccessTokenRequestRefresh : AccessTokenRequest
|
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.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests
|
namespace osu.Game.Online.API.Requests
|
||||||
@ -9,7 +8,7 @@ namespace osu.Game.Online.API.Requests
|
|||||||
public class GetSystemTitleRequest : OsuJsonWebRequest<APISystemTitle>
|
public class GetSystemTitleRequest : OsuJsonWebRequest<APISystemTitle>
|
||||||
{
|
{
|
||||||
public GetSystemTitleRequest()
|
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.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
|
||||||
@ -11,24 +9,17 @@ namespace osu.Game.Online.API.Requests
|
|||||||
public class GetUserRequest : APIRequest<APIUser>
|
public class GetUserRequest : APIRequest<APIUser>
|
||||||
{
|
{
|
||||||
public readonly string Lookup;
|
public readonly string Lookup;
|
||||||
public readonly IRulesetInfo Ruleset;
|
public readonly IRulesetInfo? Ruleset;
|
||||||
private readonly LookupType lookupType;
|
private readonly LookupType lookupType;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the currently logged-in user.
|
|
||||||
/// </summary>
|
|
||||||
public GetUserRequest()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a user from their ID.
|
/// Gets a user from their ID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId">The user to get.</param>
|
/// <param name="userId">The user to get.</param>
|
||||||
/// <param name="ruleset">The ruleset to get the user's info for.</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;
|
lookupType = LookupType.Id;
|
||||||
Ruleset = ruleset;
|
Ruleset = ruleset;
|
||||||
}
|
}
|
||||||
@ -38,14 +29,14 @@ namespace osu.Game.Online.API.Requests
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="username">The user to get.</param>
|
/// <param name="username">The user to get.</param>
|
||||||
/// <param name="ruleset">The ruleset to get the user's info for.</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;
|
Lookup = username;
|
||||||
lookupType = LookupType.Username;
|
lookupType = LookupType.Username;
|
||||||
Ruleset = ruleset;
|
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
|
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;
|
||||||
using osu.Game.Online.API.Requests;
|
using osu.Game.Online.API.Requests;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Online.Notifications;
|
|
||||||
using osu.Game.Overlays.Chat.Listing;
|
using osu.Game.Overlays.Chat.Listing;
|
||||||
|
|
||||||
namespace osu.Game.Online.Chat
|
namespace osu.Game.Online.Chat
|
||||||
@ -64,13 +63,8 @@ namespace osu.Game.Online.Chat
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IBindableList<Channel> AvailableChannels => availableChannels;
|
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 IAPIProvider api;
|
||||||
private readonly NotificationsClientConnector connector;
|
private readonly IChatClient chatClient;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private UserLookupCache users { get; set; }
|
private UserLookupCache users { get; set; }
|
||||||
@ -85,7 +79,7 @@ namespace osu.Game.Online.Chat
|
|||||||
{
|
{
|
||||||
this.api = api;
|
this.api = api;
|
||||||
|
|
||||||
connector = api.GetNotificationsConnector();
|
chatClient = api.GetChatClient();
|
||||||
|
|
||||||
CurrentChannel.ValueChanged += currentChannelChanged;
|
CurrentChannel.ValueChanged += currentChannelChanged;
|
||||||
}
|
}
|
||||||
@ -93,15 +87,11 @@ namespace osu.Game.Online.Chat
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
connector.ChannelJoined += ch => Schedule(() => joinChannel(ch));
|
chatClient.ChannelJoined += ch => Schedule(() => joinChannel(ch));
|
||||||
|
chatClient.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false));
|
||||||
connector.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false));
|
chatClient.NewMessages += msgs => Schedule(() => addMessages(msgs));
|
||||||
|
chatClient.PresenceReceived += () => Schedule(initializeChannels);
|
||||||
connector.NewMessages += msgs => Schedule(() => addMessages(msgs));
|
chatClient.RequestPresence();
|
||||||
|
|
||||||
connector.PresenceReceived += () => Schedule(initializeChannels);
|
|
||||||
|
|
||||||
connector.Start();
|
|
||||||
|
|
||||||
apiState.BindTo(api.State);
|
apiState.BindTo(api.State);
|
||||||
apiState.BindValueChanged(_ => SendAck(), true);
|
apiState.BindValueChanged(_ => SendAck(), true);
|
||||||
@ -655,7 +645,7 @@ namespace osu.Game.Online.Chat
|
|||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(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.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace osu.Game.Online
|
namespace osu.Game.Online
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -13,36 +11,36 @@ namespace osu.Game.Online
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The base URL for the website.
|
/// The base URL for the website.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string WebsiteRootUrl { get; set; }
|
public string WebsiteRootUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The endpoint for the main (osu-web) API.
|
/// The endpoint for the main (osu-web) API.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string APIEndpointUrl { get; set; }
|
public string APIEndpointUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The OAuth client secret.
|
/// The OAuth client secret.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string APIClientSecret { get; set; }
|
public string APIClientSecret { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The OAuth client ID.
|
/// The OAuth client ID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string APIClientID { get; set; }
|
public string APIClientID { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The endpoint for the SignalR spectator server.
|
/// The endpoint for the SignalR spectator server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string SpectatorEndpointUrl { get; set; }
|
public string SpectatorEndpointUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The endpoint for the SignalR multiplayer server.
|
/// The endpoint for the SignalR multiplayer server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string MultiplayerEndpointUrl { get; set; }
|
public string MultiplayerEndpointUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The endpoint for the SignalR metadata server.
|
/// The endpoint for the SignalR metadata server.
|
||||||
/// </summary>
|
/// </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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
@ -12,23 +11,20 @@ using System.Threading.Tasks;
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Framework.Extensions.TypeExtensions;
|
using osu.Framework.Extensions.TypeExtensions;
|
||||||
using osu.Framework.Logging;
|
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
|
namespace osu.Game.Online.Notifications.WebSocket
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A notifications client which receives events via a websocket.
|
/// A notifications client which receives events via a websocket.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class WebSocketNotificationsClient : NotificationsClient
|
public class WebSocketNotificationsClient : PersistentEndpointClient
|
||||||
{
|
{
|
||||||
|
public event Action<SocketMessage>? MessageReceived;
|
||||||
|
|
||||||
private readonly ClientWebSocket socket;
|
private readonly ClientWebSocket socket;
|
||||||
private readonly string endpoint;
|
private readonly string endpoint;
|
||||||
private readonly ConcurrentDictionary<long, Channel> channelsMap = new ConcurrentDictionary<long, Channel>();
|
|
||||||
|
|
||||||
public WebSocketNotificationsClient(ClientWebSocket socket, string endpoint, IAPIProvider api)
|
public WebSocketNotificationsClient(ClientWebSocket socket, string endpoint)
|
||||||
: base(api)
|
|
||||||
{
|
{
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
this.endpoint = endpoint;
|
this.endpoint = endpoint;
|
||||||
@ -37,11 +33,7 @@ namespace osu.Game.Online.Notifications.WebSocket
|
|||||||
public override async Task ConnectAsync(CancellationToken cancellationToken)
|
public override async Task ConnectAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await socket.ConnectAsync(new Uri(endpoint), cancellationToken).ConfigureAwait(false);
|
await socket.ConnectAsync(new Uri(endpoint), cancellationToken).ConfigureAwait(false);
|
||||||
await sendMessage(new StartChatRequest(), CancellationToken.None).ConfigureAwait(false);
|
|
||||||
|
|
||||||
runReadLoop(cancellationToken);
|
runReadLoop(cancellationToken);
|
||||||
|
|
||||||
await base.ConnectAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void runReadLoop(CancellationToken cancellationToken) => Task.Run(async () =>
|
private void runReadLoop(CancellationToken cancellationToken) => Task.Run(async () =>
|
||||||
@ -73,7 +65,7 @@ namespace osu.Game.Online.Notifications.WebSocket
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
await onMessageReceivedAsync(message).ConfigureAwait(false);
|
MessageReceived?.Invoke(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
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)
|
if (socket.State != WebSocketState.Open)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
|
await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken ?? CancellationToken.None).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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async ValueTask DisposeAsync()
|
public override async ValueTask DisposeAsync()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -13,26 +14,26 @@ namespace osu.Game.Online.Notifications.WebSocket
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A connector for <see cref="WebSocketNotificationsClient"/>s that receive events via a websocket.
|
/// A connector for <see cref="WebSocketNotificationsClient"/>s that receive events via a websocket.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class WebSocketNotificationsClientConnector : NotificationsClientConnector
|
public class WebSocketNotificationsClientConnector : PersistentEndpointClientConnector, INotificationsClient
|
||||||
{
|
{
|
||||||
|
public event Action<SocketMessage>? MessageReceived;
|
||||||
|
|
||||||
private readonly IAPIProvider api;
|
private readonly IAPIProvider api;
|
||||||
|
|
||||||
public WebSocketNotificationsClientConnector(IAPIProvider api)
|
public WebSocketNotificationsClientConnector(IAPIProvider api)
|
||||||
: base(api)
|
: base(api)
|
||||||
{
|
{
|
||||||
this.api = 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();
|
var req = new GetNotificationsRequest();
|
||||||
req.Success += bundle => tcs.SetResult(bundle.Endpoint);
|
// must use `PerformAsync()`, since we may not be fully online yet
|
||||||
req.Failure += ex => tcs.SetException(ex);
|
// (see `APIState.RequiresSecondFactorAuth` - in this state queued requests will not execute).
|
||||||
api.Queue(req);
|
await api.PerformAsync(req).ConfigureAwait(false);
|
||||||
|
string endpoint = req.Response!.Endpoint;
|
||||||
string endpoint = await tcs.Task.ConfigureAwait(false);
|
|
||||||
|
|
||||||
ClientWebSocket socket = new ClientWebSocket();
|
ClientWebSocket socket = new ClientWebSocket();
|
||||||
socket.Options.SetRequestHeader(@"Authorization", @$"Bearer {api.AccessToken}");
|
socket.Options.SetRequestHeader(@"Authorization", @$"Bearer {api.AccessToken}");
|
||||||
@ -40,7 +41,17 @@ namespace osu.Game.Online.Notifications.WebSocket
|
|||||||
if (socket.Options.Proxy != null)
|
if (socket.Options.Proxy != null)
|
||||||
socket.Options.Proxy.Credentials = CredentialCache.DefaultCredentials;
|
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.API;
|
||||||
using osu.Game.Online.Metadata;
|
using osu.Game.Online.Metadata;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Online.Notifications.WebSocket;
|
||||||
using osu.Game.Online.Spectator;
|
using osu.Game.Online.Spectator;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Notifications;
|
using osu.Game.Overlays.Notifications;
|
||||||
@ -25,6 +26,8 @@ namespace osu.Game.Online
|
|||||||
{
|
{
|
||||||
private readonly Func<IScreen> getCurrentScreen;
|
private readonly Func<IScreen> getCurrentScreen;
|
||||||
|
|
||||||
|
private INotificationsClient notificationsClient = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private MultiplayerClient multiplayerClient { get; set; } = null!;
|
private MultiplayerClient multiplayerClient { get; set; } = null!;
|
||||||
|
|
||||||
@ -55,9 +58,11 @@ namespace osu.Game.Online
|
|||||||
private void load(IAPIProvider api)
|
private void load(IAPIProvider api)
|
||||||
{
|
{
|
||||||
apiState = api.State.GetBoundCopy();
|
apiState = api.State.GetBoundCopy();
|
||||||
|
notificationsClient = api.NotificationsClient;
|
||||||
multiplayerState = multiplayerClient.IsConnected.GetBoundCopy();
|
multiplayerState = multiplayerClient.IsConnected.GetBoundCopy();
|
||||||
spectatorState = spectatorClient.IsConnected.GetBoundCopy();
|
spectatorState = spectatorClient.IsConnected.GetBoundCopy();
|
||||||
|
|
||||||
|
notificationsClient.MessageReceived += notifyAboutForcedDisconnection;
|
||||||
multiplayerClient.Disconnecting += notifyAboutForcedDisconnection;
|
multiplayerClient.Disconnecting += notifyAboutForcedDisconnection;
|
||||||
spectatorClient.Disconnecting += notifyAboutForcedDisconnection;
|
spectatorClient.Disconnecting += notifyAboutForcedDisconnection;
|
||||||
metadataClient.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)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
if (notificationsClient.IsNotNull())
|
||||||
|
notificationsClient.MessageReceived += notifyAboutForcedDisconnection;
|
||||||
|
|
||||||
if (spectatorClient.IsNotNull())
|
if (spectatorClient.IsNotNull())
|
||||||
spectatorClient.Disconnecting -= notifyAboutForcedDisconnection;
|
spectatorClient.Disconnecting -= notifyAboutForcedDisconnection;
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -79,10 +80,14 @@ namespace osu.Game.Online
|
|||||||
|
|
||||||
case APIState.Failing:
|
case APIState.Failing:
|
||||||
case APIState.Connecting:
|
case APIState.Connecting:
|
||||||
|
case APIState.RequiresSecondFactorAuth:
|
||||||
PopContentOut(Content);
|
PopContentOut(Content);
|
||||||
LoadingSpinner.Show();
|
LoadingSpinner.Show();
|
||||||
placeholder.FadeOut(transform_duration / 2, Easing.OutQuint);
|
placeholder.FadeOut(transform_duration / 2, Easing.OutQuint);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -69,6 +69,7 @@ namespace osu.Game.Online
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case APIState.Online:
|
case APIState.Online:
|
||||||
|
case APIState.RequiresSecondFactorAuth:
|
||||||
await connect().ConfigureAwait(true);
|
await connect().ConfigureAwait(true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -83,7 +84,7 @@ namespace osu.Game.Online
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (apiState.Value == APIState.Online)
|
while (apiState.Value == APIState.RequiresSecondFactorAuth || apiState.Value == APIState.Online)
|
||||||
{
|
{
|
||||||
// ensure any previous connection was disposed.
|
// ensure any previous connection was disposed.
|
||||||
// this will also create a new cancellation token source.
|
// this will also create a new cancellation token source.
|
||||||
|
@ -264,13 +264,12 @@ namespace osu.Game.Online.Spectator
|
|||||||
{
|
{
|
||||||
Debug.Assert(ThreadSafety.IsUpdateThread);
|
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||||
|
|
||||||
if (watchedUsersRefCounts.ContainsKey(userId))
|
if (!watchedUsersRefCounts.TryAdd(userId, 1))
|
||||||
{
|
{
|
||||||
watchedUsersRefCounts[userId]++;
|
watchedUsersRefCounts[userId]++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchedUsersRefCounts.Add(userId, 1);
|
|
||||||
WatchUserInternal(userId);
|
WatchUserInternal(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ namespace osu.Game
|
|||||||
public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild;
|
public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild;
|
||||||
|
|
||||||
public virtual EndpointConfiguration CreateEndpoints() =>
|
public virtual EndpointConfiguration CreateEndpoints() =>
|
||||||
UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ExperimentalEndpointConfiguration();
|
UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
|
||||||
|
|
||||||
public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version();
|
public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version();
|
||||||
|
|
||||||
@ -340,10 +340,6 @@ namespace osu.Game
|
|||||||
dependencies.Cache(beatmapCache = new BeatmapLookupCache());
|
dependencies.Cache(beatmapCache = new BeatmapLookupCache());
|
||||||
base.Content.Add(beatmapCache);
|
base.Content.Add(beatmapCache);
|
||||||
|
|
||||||
var scorePerformanceManager = new ScorePerformanceCache();
|
|
||||||
dependencies.Cache(scorePerformanceManager);
|
|
||||||
base.Content.Add(scorePerformanceManager);
|
|
||||||
|
|
||||||
dependencies.CacheAs<IRulesetConfigCache>(rulesetConfigCache = new RulesetConfigCache(realm, RulesetStore));
|
dependencies.CacheAs<IRulesetConfigCache>(rulesetConfigCache = new RulesetConfigCache(realm, RulesetStore));
|
||||||
|
|
||||||
var powerStatus = CreateBatteryInfo();
|
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.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.LocalisationExtensions;
|
using osu.Framework.Extensions.LocalisationExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -28,28 +27,30 @@ namespace osu.Game.Overlays.AccountCreation
|
|||||||
{
|
{
|
||||||
public partial class ScreenEntry : AccountCreationScreen
|
public partial class ScreenEntry : AccountCreationScreen
|
||||||
{
|
{
|
||||||
private ErrorTextFlowContainer usernameDescription;
|
private ErrorTextFlowContainer usernameDescription = null!;
|
||||||
private ErrorTextFlowContainer emailAddressDescription;
|
private ErrorTextFlowContainer emailAddressDescription = null!;
|
||||||
private ErrorTextFlowContainer passwordDescription;
|
private ErrorTextFlowContainer passwordDescription = null!;
|
||||||
|
|
||||||
private OsuTextBox usernameTextBox;
|
private OsuTextBox usernameTextBox = null!;
|
||||||
private OsuTextBox emailTextBox;
|
private OsuTextBox emailTextBox = null!;
|
||||||
private OsuPasswordTextBox passwordTextBox;
|
private OsuPasswordTextBox passwordTextBox = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IAPIProvider api { get; set; }
|
private IAPIProvider api { get; set; } = null!;
|
||||||
|
|
||||||
private ShakeContainer registerShake;
|
private IBindable<APIState> apiState = null!;
|
||||||
private ITextPart characterCheckText;
|
|
||||||
|
|
||||||
private OsuTextBox[] textboxes;
|
private ShakeContainer registerShake = null!;
|
||||||
private LoadingLayer loadingLayer;
|
private ITextPart characterCheckText = null!;
|
||||||
|
|
||||||
|
private OsuTextBox[] textboxes = null!;
|
||||||
|
private LoadingLayer loadingLayer = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private GameHost host { get; set; }
|
private GameHost? host { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuGame game { get; set; }
|
private OsuGame? game { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
@ -144,6 +145,8 @@ namespace osu.Game.Overlays.AccountCreation
|
|||||||
|
|
||||||
passwordTextBox.Current.BindValueChanged(_ => updateCharacterCheckTextColour(), true);
|
passwordTextBox.Current.BindValueChanged(_ => updateCharacterCheckTextColour(), true);
|
||||||
characterCheckText.DrawablePartsRecreated += _ => updateCharacterCheckTextColour();
|
characterCheckText.DrawablePartsRecreated += _ => updateCharacterCheckTextColour();
|
||||||
|
|
||||||
|
apiState = api.State.GetBoundCopy();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateCharacterCheckTextColour()
|
private void updateCharacterCheckTextColour()
|
||||||
@ -180,7 +183,7 @@ namespace osu.Game.Overlays.AccountCreation
|
|||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
bool success;
|
bool success;
|
||||||
RegistrationRequest.RegistrationRequestErrors errors = null;
|
RegistrationRequest.RegistrationRequestErrors? errors = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -210,7 +213,7 @@ namespace osu.Game.Overlays.AccountCreation
|
|||||||
if (!string.IsNullOrEmpty(errors.Message))
|
if (!string.IsNullOrEmpty(errors.Message))
|
||||||
passwordDescription.AddErrors(new[] { 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
|
else
|
||||||
@ -223,6 +226,12 @@ namespace osu.Game.Overlays.AccountCreation
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apiState.BindValueChanged(state =>
|
||||||
|
{
|
||||||
|
if (state.NewValue == APIState.RequiresSecondFactorAuth)
|
||||||
|
this.Push(new ScreenEmailVerification());
|
||||||
|
});
|
||||||
|
|
||||||
api.Login(usernameTextBox.Text, passwordTextBox.Text);
|
api.Login(usernameTextBox.Text, passwordTextBox.Text);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -241,6 +250,6 @@ namespace osu.Game.Overlays.AccountCreation
|
|||||||
return false;
|
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.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -23,14 +21,14 @@ namespace osu.Game.Overlays.AccountCreation
|
|||||||
{
|
{
|
||||||
public partial class ScreenWarning : AccountCreationScreen
|
public partial class ScreenWarning : AccountCreationScreen
|
||||||
{
|
{
|
||||||
private OsuTextFlowContainer multiAccountExplanationText;
|
private OsuTextFlowContainer multiAccountExplanationText = null!;
|
||||||
private LinkFlowContainer furtherAssistance;
|
private LinkFlowContainer furtherAssistance = null!;
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved]
|
||||||
private IAPIProvider api { get; set; }
|
private IAPIProvider? api { get; set; }
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved]
|
||||||
private OsuGame game { get; set; }
|
private OsuGame? game { get; set; }
|
||||||
|
|
||||||
private const string help_centre_url = "/help/wiki/Help_Centre#login";
|
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.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
using System;
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
@ -24,7 +23,9 @@ namespace osu.Game.Overlays
|
|||||||
{
|
{
|
||||||
private const float transition_time = 400;
|
private const float transition_time = 400;
|
||||||
|
|
||||||
private ScreenWelcome welcomeScreen;
|
private ScreenWelcome welcomeScreen = null!;
|
||||||
|
|
||||||
|
private ScheduledDelegate? scheduledHide;
|
||||||
|
|
||||||
public AccountCreationOverlay()
|
public AccountCreationOverlay()
|
||||||
{
|
{
|
||||||
@ -107,8 +108,6 @@ namespace osu.Game.Overlays
|
|||||||
this.FadeOut(100);
|
this.FadeOut(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ScheduledDelegate scheduledHide;
|
|
||||||
|
|
||||||
private void apiStateChanged(ValueChangedEvent<APIState> state)
|
private void apiStateChanged(ValueChangedEvent<APIState> state)
|
||||||
{
|
{
|
||||||
switch (state.NewValue)
|
switch (state.NewValue)
|
||||||
@ -118,12 +117,16 @@ namespace osu.Game.Overlays
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case APIState.Connecting:
|
case APIState.Connecting:
|
||||||
|
case APIState.RequiresSecondFactorAuth:
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case APIState.Online:
|
case APIState.Online:
|
||||||
scheduledHide?.Cancel();
|
scheduledHide?.Cancel();
|
||||||
scheduledHide = Schedule(Hide);
|
scheduledHide = Schedule(Hide);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Comments
|
|||||||
|
|
||||||
public Color4 AccentColour { get; set; }
|
public Color4 AccentColour { get; set; }
|
||||||
|
|
||||||
protected override IEnumerable<Drawable> EffectTargets => null;
|
protected override IEnumerable<Drawable> EffectTargets => Enumerable.Empty<Drawable>();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IAPIProvider api { get; set; }
|
private IAPIProvider api { get; set; }
|
||||||
|
@ -32,13 +32,7 @@ namespace osu.Game.Overlays.Login
|
|||||||
|
|
||||||
public Action? RequestHide;
|
public Action? RequestHide;
|
||||||
|
|
||||||
private void performLogin()
|
public override bool AcceptsFocus => true;
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text))
|
|
||||||
api.Login(username.Text, password.Text);
|
|
||||||
else
|
|
||||||
shakeSignIn.Shake();
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader(permitNulls: true)]
|
[BackgroundDependencyLoader(permitNulls: true)]
|
||||||
private void load(OsuConfigManager config, AccountCreationOverlay accountCreation)
|
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;
|
protected override bool OnClick(ClickEvent e) => true;
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Login
|
|||||||
{
|
{
|
||||||
private bool bounding = true;
|
private bool bounding = true;
|
||||||
|
|
||||||
private LoginForm? form;
|
private Drawable? form;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuColour colours { get; set; } = null!;
|
private OsuColour colours { get; set; } = null!;
|
||||||
@ -81,6 +81,10 @@ namespace osu.Game.Overlays.Login
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case APIState.RequiresSecondFactorAuth:
|
||||||
|
Child = form = new SecondFactorAuthForm();
|
||||||
|
break;
|
||||||
|
|
||||||
case APIState.Failing:
|
case APIState.Failing:
|
||||||
case APIState.Connecting:
|
case APIState.Connecting:
|
||||||
LinkFlowContainer linkFlow;
|
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