diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index b8dc201559..99906f0895 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,13 +3,13 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
- "version": "2022.2.3",
+ "version": "2023.3.3",
"commands": [
"jb"
]
},
"nvika": {
- "version": "2.2.0",
+ "version": "3.0.0",
"commands": [
"nvika"
]
diff --git a/.globalconfig b/.globalconfig
index a7b652c454..a4d4707f9b 100644
--- a/.globalconfig
+++ b/.globalconfig
@@ -1,5 +1,3 @@
-is_global = true
-
# .NET Code Style
# IDE styles reference: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/
@@ -56,4 +54,4 @@ dotnet_diagnostic.RS0030.severity = error
# Temporarily disable analysing CanBeNull = true in NRT contexts due to mobile issues.
# See: https://github.com/ppy/osu/pull/19677
-dotnet_diagnostic.OSUF001.severity = none
\ No newline at end of file
+dotnet_diagnostic.OSUF001.severity = none
diff --git a/Directory.Build.props b/Directory.Build.props
index 734374c840..b08283f071 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -35,7 +35,7 @@
https://github.com/ppy/osu
Automated release.
ppy Pty Ltd
- Copyright (c) 2022 ppy Pty Ltd
+ Copyright (c) 2024 ppy Pty Ltd
osu game
diff --git a/LICENCE b/LICENCE
index d3e7537cef..3bb8b62d5d 100644
--- a/LICENCE
+++ b/LICENCE
@@ -1,4 +1,4 @@
-Copyright (c) 2022 ppy Pty Ltd .
+Copyright (c) 2024 ppy Pty Ltd .
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index d5dc0723af..d7e710f392 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ A few resources are available as starting points to getting involved and underst
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
-- Track our current efforts [towards full "ranked play" support](https://github.com/orgs/ppy/projects/13?query=is%3Aopen+sort%3Aupdated-desc).
+- Track our current efforts [towards improving the game](https://github.com/orgs/ppy/projects/7/views/6).
## Running osu!
diff --git a/Templates/osu.Game.Templates.csproj b/Templates/osu.Game.Templates.csproj
index b8c3ad373a..186a6093f5 100644
--- a/Templates/osu.Game.Templates.csproj
+++ b/Templates/osu.Game.Templates.csproj
@@ -1,4 +1,4 @@
-
+
Template
ppy.osu.Game.Templates
@@ -8,7 +8,7 @@
https://github.com/ppy/osu/blob/master/Templates
https://github.com/ppy/osu
Automated release.
- Copyright (c) 2022 ppy Pty Ltd
+ Copyright (c) 2024 ppy Pty Ltd
Templates to use when creating a ruleset for consumption in osu!.
dotnet-new;templates;osu
netstandard2.1
diff --git a/assets/lazer-nuget.png b/assets/lazer-nuget.png
index c2a587fdc2..fed2f45149 100644
Binary files a/assets/lazer-nuget.png and b/assets/lazer-nuget.png differ
diff --git a/assets/lazer.png b/assets/lazer.png
index 1e40e844cc..2ee44225bf 100644
Binary files a/assets/lazer.png and b/assets/lazer.png differ
diff --git a/osu.Android.props b/osu.Android.props
index a7376aa5a7..d944e2ce8e 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -10,7 +10,7 @@
true
-
+
diff --git a/osu.Android/Resources/drawable/ic_launcher_background.xml b/osu.Android/Resources/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000000..1af30228ec
--- /dev/null
+++ b/osu.Android/Resources/drawable/ic_launcher_background.xml
@@ -0,0 +1,618 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/osu.Android/Resources/drawable/lazer.png b/osu.Android/Resources/drawable/lazer.png
deleted file mode 100644
index fc7aa8a092..0000000000
Binary files a/osu.Android/Resources/drawable/lazer.png and /dev/null differ
diff --git a/osu.Android/Resources/drawable/monochrome.xml b/osu.Android/Resources/drawable/monochrome.xml
new file mode 100644
index 0000000000..e12af03bfb
--- /dev/null
+++ b/osu.Android/Resources/drawable/monochrome.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
diff --git a/osu.Android/Resources/mipmap-anydpi-v26/ic_launcher.xml b/osu.Android/Resources/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000..7133c9c861
--- /dev/null
+++ b/osu.Android/Resources/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/osu.Android/Resources/mipmap-hdpi/ic_launcher.png b/osu.Android/Resources/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..7870430484
Binary files /dev/null and b/osu.Android/Resources/mipmap-hdpi/ic_launcher.png differ
diff --git a/osu.Android/Resources/mipmap-hdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..b2ec3e49da
Binary files /dev/null and b/osu.Android/Resources/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/osu.Android/Resources/mipmap-mdpi/ic_launcher.png b/osu.Android/Resources/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..2a01d8f781
Binary files /dev/null and b/osu.Android/Resources/mipmap-mdpi/ic_launcher.png differ
diff --git a/osu.Android/Resources/mipmap-mdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..e22f256562
Binary files /dev/null and b/osu.Android/Resources/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/osu.Android/Resources/mipmap-xhdpi/ic_launcher.png b/osu.Android/Resources/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..b5e1a9e379
Binary files /dev/null and b/osu.Android/Resources/mipmap-xhdpi/ic_launcher.png differ
diff --git a/osu.Android/Resources/mipmap-xhdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..1cc3fa9072
Binary files /dev/null and b/osu.Android/Resources/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/osu.Android/Resources/mipmap-xxhdpi/ic_launcher.png b/osu.Android/Resources/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..8a37b0449e
Binary files /dev/null and b/osu.Android/Resources/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/osu.Android/Resources/mipmap-xxhdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..1b856a31b2
Binary files /dev/null and b/osu.Android/Resources/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher.png b/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..65751e15c9
Binary files /dev/null and b/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000..05c6829a47
Binary files /dev/null and b/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/osu.Desktop/lazer.ico b/osu.Desktop/lazer.ico
old mode 100755
new mode 100644
index a6aa8abb9f..f84866b8e9
Binary files a/osu.Desktop/lazer.ico and b/osu.Desktop/lazer.ico differ
diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec
index db58c325bd..f85698680e 100644
--- a/osu.Desktop/osu.nuspec
+++ b/osu.Desktop/osu.nuspec
@@ -7,11 +7,12 @@
ppy Pty Ltd
Dean Herbert
https://osu.ppy.sh/
- https://puu.sh/tYyXZ/9a01a5d1b0.ico
+ https://github.com/ppy/osu/blob/master/assets/lazer-nuget.png?raw=true
+ icon.png
false
A free-to-win rhythm game. Rhythm is just a *click* away!
testing
- Copyright (c) 2022 ppy Pty Ltd
+ Copyright (c) 2024 ppy Pty Ltd
en-AU
@@ -19,5 +20,6 @@
+
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
index fee3ba3e39..db04142915 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
@@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
foreach (var stage in stages)
{
- for (int i = 0; i < stage.Columns.Count; i++)
+ for (int i = 0; i < stage.Columns.Length; i++)
{
var obj = new Note { Column = i, StartTime = Time.Current + 2000 };
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
@@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
foreach (var stage in stages)
{
- for (int i = 0; i < stage.Columns.Count; i++)
+ for (int i = 0; i < stage.Columns.Length; i++)
{
var obj = new HoldNote { Column = i, StartTime = Time.Current + 2000, Duration = 500 };
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
index 050b302bd8..88d6a19822 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
@@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public abstract int KeyCount { get; }
public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1; // TODO: Implement the mania key mod score multiplier
+ public override bool Ranked => UsesDefaultConfiguration;
public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
{
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs
index d9de06a811..189c4b3a5f 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs
@@ -8,5 +8,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public class ManiaModHardRock : ModHardRock
{
public override double ScoreMultiplier => 1;
+ public override bool Ranked => false;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs
index 31f52610e9..7dd0c499da 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs
@@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Name => "One Key";
public override string Acronym => "1K";
public override LocalisableString Description => @"Play with one key.";
+ public override bool Ranked => false;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs
index 67e65b887a..a6c57d4597 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs
@@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Name => "Ten Keys";
public override string Acronym => "10K";
public override LocalisableString Description => @"Play with ten keys.";
+ public override bool Ranked => false;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs
index 0f8148d252..0d04395a52 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs
@@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Name => "Two Keys";
public override string Acronym => "2K";
public override LocalisableString Description => @"Play with two keys.";
+ public override bool Ranked => false;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs
index 0f8af7940c..c83b0979ee 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs
@@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Name => "Three Keys";
public override string Acronym => "3K";
public override LocalisableString Description => @"Play with three keys.";
+ public override bool Ranked => false;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
index f9690b4298..cc7e270dda 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
@@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public class ManiaModMirror : ModMirror, IApplicableToBeatmap
{
public override LocalisableString Description => "Notes are flipped horizontally.";
+ public override bool Ranked => UsesDefaultConfiguration;
public void ApplyToBeatmap(IBeatmap beatmap)
{
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
index a191dee1ca..0052fd8b78 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
@@ -17,7 +17,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
- public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
+ public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement
{
private const float judgement_y_position = 160;
diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
index 8734f8ac8a..1593e8e76f 100644
--- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
+++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
@@ -4,8 +4,6 @@
#nullable disable
using System;
-using System.Collections.Generic;
-using System.Linq;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -28,20 +26,21 @@ namespace osu.Game.Rulesets.Mania.UI
///
/// All contents added to this .
///
- public IReadOnlyList Content => columns.Children.Select(c => c.Count == 0 ? null : (TContent)c.Child).ToList();
+ public TContent[] Content { get; }
- private readonly FillFlowContainer columns;
+ private readonly FillFlowContainer> columns;
private readonly StageDefinition stageDefinition;
public ColumnFlow(StageDefinition stageDefinition)
{
this.stageDefinition = stageDefinition;
+ Content = new TContent[stageDefinition.Columns];
AutoSizeAxes = Axes.X;
Masking = true;
- InternalChild = columns = new FillFlowContainer
+ InternalChild = columns = new FillFlowContainer>
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
@@ -49,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.UI
};
for (int i = 0; i < stageDefinition.Columns; i++)
- columns.Add(new Container { RelativeSizeAxes = Axes.Y });
+ columns.Add(new Container { RelativeSizeAxes = Axes.Y });
}
private ISkinSource currentSkin;
@@ -102,7 +101,10 @@ namespace osu.Game.Rulesets.Mania.UI
///
/// The index of the column to set the content of.
/// The content.
- 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()
{
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index 314d199944..0d36f51943 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
@@ -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 stageDefinitions)
{
@@ -71,7 +80,7 @@ namespace osu.Game.Rulesets.Mania.UI
stages.Add(newStage);
AddNested(newStage);
- firstColumnIndex += newStage.Columns.Count;
+ firstColumnIndex += newStage.Columns.Length;
}
}
@@ -125,9 +134,9 @@ namespace osu.Game.Rulesets.Mania.UI
foreach (var stage in stages)
{
- if (index >= stage.Columns.Count)
+ if (index >= stage.Columns.Length)
{
- index -= stage.Columns.Count;
+ index -= stage.Columns.Length;
continue;
}
@@ -140,7 +149,7 @@ namespace osu.Game.Rulesets.Mania.UI
///
/// Retrieves the total amount of columns across all stages in this playfield.
///
- public int TotalColumns => stages.Sum(s => s.Columns.Count);
+ public int TotalColumns => stages.Sum(s => s.Columns.Length);
private Stage getStageByColumn(int column)
{
@@ -148,7 +157,7 @@ namespace osu.Game.Rulesets.Mania.UI
foreach (var stage in stages)
{
- sum += stage.Columns.Count;
+ sum += stage.Columns.Length;
if (sum > column)
return stage;
}
diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
index 36286940a8..a4a09c9a82 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
@@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.UI
public const float HIT_TARGET_POSITION = 110;
- public IReadOnlyList Columns => columnFlow.Content;
+ public Column[] Columns => columnFlow.Content;
private readonly ColumnFlow columnFlow;
private readonly JudgementContainer judgements;
@@ -45,7 +44,16 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly Drawable barLineContainer;
- public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Columns.Any(c => c.ReceivePositionalInputAt(screenSpacePos));
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
+ {
+ foreach (var c in Columns)
+ {
+ if (c.ReceivePositionalInputAt(screenSpacePos))
+ return true;
+ }
+
+ return false;
+ }
private readonly int firstColumnIndex;
@@ -184,13 +192,13 @@ namespace osu.Game.Rulesets.Mania.UI
NewResult += OnNewResult;
}
- public override void Add(HitObject hitObject) => Columns.ElementAt(((ManiaHitObject)hitObject).Column - firstColumnIndex).Add(hitObject);
+ public override void Add(HitObject hitObject) => Columns[((ManiaHitObject)hitObject).Column - firstColumnIndex].Add(hitObject);
- public override bool Remove(HitObject hitObject) => Columns.ElementAt(((ManiaHitObject)hitObject).Column - firstColumnIndex).Remove(hitObject);
+ public override bool Remove(HitObject hitObject) => Columns[((ManiaHitObject)hitObject).Column - firstColumnIndex].Remove(hitObject);
- public override void Add(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Add(h);
+ public override void Add(DrawableHitObject h) => Columns[((ManiaHitObject)h.HitObject).Column - firstColumnIndex].Add(h);
- public override bool Remove(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Remove(h);
+ public override bool Remove(DrawableHitObject h) => Columns[((ManiaHitObject)h.HitObject).Column - firstColumnIndex].Remove(h);
public void Add(BarLine barLine) => base.Add(barLine);
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs
index 8234381283..2b53554ed1 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs
@@ -206,7 +206,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
AddStep($"click context menu item \"{contextMenuText}\"", () =>
{
- MenuItem item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == "Curve type")?.Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
+ MenuItem item = visualiser.ContextMenuItems!.FirstOrDefault(menuItem => menuItem.Text.Value == "Curve type")?.Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
item?.Action.Value?.Invoke();
});
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs
index d78c32aa6a..77ef4627cb 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new PathControlPoint(new Vector2(-128, 0), PathType.LINEAR) // absolute position: (0, 128)
}
},
- RepeatCount = 1
+ RepeatCount = 2
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
return slider;
@@ -45,7 +45,9 @@ namespace osu.Game.Rulesets.Osu.Tests
OsuHitObjectGenerationUtils.ReflectHorizontallyAlongPlayfield(slider);
Assert.That(slider.Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 128, 128)));
- Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 0, 128)));
+ Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 128, 128)));
+ Assert.That(slider.NestedHitObjects.OfType().First().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X - 0, 128)));
+ Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(OsuPlayfield.BASE_SIZE.X, 128)));
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
{
new Vector2(),
@@ -62,7 +64,9 @@ namespace osu.Game.Rulesets.Osu.Tests
OsuHitObjectGenerationUtils.ReflectVerticallyAlongPlayfield(slider);
Assert.That(slider.Position, Is.EqualTo(new Vector2(128, OsuPlayfield.BASE_SIZE.Y - 128)));
- Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(0, OsuPlayfield.BASE_SIZE.Y - 128)));
+ Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(128, OsuPlayfield.BASE_SIZE.Y - 128)));
+ Assert.That(slider.NestedHitObjects.OfType().First().Position, Is.EqualTo(new Vector2(0, OsuPlayfield.BASE_SIZE.Y - 128)));
+ Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(0, OsuPlayfield.BASE_SIZE.Y - 128)));
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
{
new Vector2(),
@@ -79,7 +83,9 @@ namespace osu.Game.Rulesets.Osu.Tests
OsuHitObjectGenerationUtils.FlipSliderInPlaceHorizontally(slider);
Assert.That(slider.Position, Is.EqualTo(new Vector2(128, 128)));
- Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(256, 128)));
+ Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(128, 128)));
+ Assert.That(slider.NestedHitObjects.OfType().First().Position, Is.EqualTo(new Vector2(256, 128)));
+ Assert.That(slider.NestedHitObjects.OfType().Single().Position, Is.EqualTo(new Vector2(256, 128)));
Assert.That(slider.Path.ControlPoints.Select(point => point.Position), Is.EquivalentTo(new[]
{
new Vector2(),
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
index eefaa3cae3..28c9d71139 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
@@ -183,7 +183,7 @@ namespace osu.Game.Rulesets.Osu.Tests
break;
}
- hitObjectContainer.Add(drawableObject);
+ hitObjectContainer.Add(drawableObject!);
followPointRenderer.AddFollowPoints(objects[i]);
}
});
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
index 09b906cb10..c624fbbe73 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -173,6 +174,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public IEnumerable AllSources => new[] { this };
+ [CanBeNull]
public event Action SourceChanged;
private bool enabled = true;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
index f691731afe..df9544b71e 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
@@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => @"Spinners will be automatically completed.";
public override double ScoreMultiplier => 0.9;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTargetPractice) };
+ public override bool Ranked => UsesDefaultConfiguration;
public void ApplyToDrawableHitObject(DrawableHitObject hitObject)
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs
index f1468d414e..917685cdad 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs
@@ -10,5 +10,6 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModTouchDevice : ModTouchDevice
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray();
+ public override bool Ranked => UsesDefaultConfiguration;
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index c0ff258352..6d492e7b08 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -239,11 +239,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (Tracking.Value && Time.Current >= HitObject.StartTime)
{
// keep the sliding sample playing at the current tracking position
- if (!slidingSample.IsPlaying)
+ if (!slidingSample.RequestedPlaying)
slidingSample.Play();
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball));
}
- else if (slidingSample.IsPlaying)
+ else if (slidingSample.IsPlaying || slidingSample.RequestedPlaying)
slidingSample.Stop();
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
index d64fb0bcc6..73c061afbd 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public const double ANIM_DURATION = 150;
- private const float default_tick_size = 16;
+ public const float DEFAULT_TICK_SIZE = 16;
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
@@ -44,8 +44,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
Masking = true,
Origin = Anchor.Centre,
- Size = new Vector2(default_tick_size),
- BorderThickness = default_tick_size / 4,
+ Size = new Vector2(DEFAULT_TICK_SIZE),
+ BorderThickness = DEFAULT_TICK_SIZE / 4,
BorderColour = Color4.White,
Child = new Box
{
@@ -88,8 +88,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break;
case ArmedState.Miss:
- this.FadeOut(ANIM_DURATION);
- this.TransformBindableTo(AccentColour, Color4.Red, 0);
+ this.FadeOut(ANIM_DURATION, Easing.OutQuint);
break;
case ArmedState.Hit:
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 3679bc9775..11120e49b5 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
if (tracking.NewValue)
{
- if (!spinningSample.IsPlaying)
+ if (!spinningSample.RequestedPlaying)
spinningSample.Play();
spinningSample.VolumeTo(1, 300);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs
index 95896c7c91..148cf79337 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs
@@ -215,8 +215,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (headCircleHitAction == null)
timeToAcceptAnyKeyAfter = null;
- var actions = slider.OsuActionInputManager?.PressedActions;
-
// if the head circle was hit with a specific key, tracking should only occur while that key is pressed.
if (headCircleHitAction != null && timeToAcceptAnyKeyAfter == null)
{
@@ -227,6 +225,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
timeToAcceptAnyKeyAfter = Time.Current;
}
+ if (slider.OsuActionInputManager == null)
+ return;
+
+ lastPressedActions.Clear();
+ bool validTrackingAction = false;
+
+ foreach (OsuAction action in slider.OsuActionInputManager.PressedActions)
+ {
+ if (isValidTrackingAction(action))
+ validTrackingAction = true;
+
+ lastPressedActions.Add(action);
+ }
+
Tracking =
// even in an edge case where current time has exceeded the slider's time, we may not have finished judging.
// we don't want to potentially update from Tracking=true to Tracking=false at this point.
@@ -234,11 +246,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// in valid position range
&& isValidTrackingPosition
// valid action
- && (actions?.Any(isValidTrackingAction) ?? false);
-
- lastPressedActions.Clear();
- if (actions != null)
- lastPressedActions.AddRange(actions);
+ && validTrackingAction;
}
private OsuAction? getInitialHitAction() => slider.HeadCircle?.HitAction;
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 032f105ded..506145568e 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects
set
{
repeatCount = value;
- endPositionCache.Invalidate();
+ updateNestedPositions();
}
}
@@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public Slider()
{
SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples();
- Path.Version.ValueChanged += _ => endPositionCache.Invalidate();
+ Path.Version.ValueChanged += _ => updateNestedPositions();
}
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs
index ccd388192e..e472de1dfe 100644
--- a/osu.Game.Rulesets.Osu/OsuInputManager.cs
+++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs
@@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu
base.ReloadMappings(realmKeyBindings);
if (!AllowGameplayInputs)
- KeyBindings = KeyBindings.Where(b => b.GetAction() == OsuAction.Smoke).ToList();
+ KeyBindings = KeyBindings.Where(static b => b.GetAction() == OsuAction.Smoke).ToList();
}
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 0496d1f680..6752712be1 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -28,6 +28,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.Skinning.Argon;
+using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Rulesets.Osu.UI;
@@ -254,6 +255,9 @@ namespace osu.Game.Rulesets.Osu
case ArgonSkin:
return new OsuArgonSkinTransformer(skin);
+
+ case TrianglesSkin:
+ return new OsuTrianglesSkinTransformer(skin);
}
return null;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
index 9a5abba4fb..83992fc785 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
@@ -16,7 +16,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
- public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
+ public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement
{
private RingExplosion? ringExplosion;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPieceSliderTickMiss.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPieceSliderTickMiss.cs
new file mode 100644
index 0000000000..bd883d6e4c
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPieceSliderTickMiss.cs
@@ -0,0 +1,51 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.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();
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderScorePoint.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderScorePoint.cs
index 7479c2aced..e9ee432bac 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderScorePoint.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderScorePoint.cs
@@ -16,14 +16,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
private Bindable accentColour = null!;
- private const float size = 12;
+ public const float SIZE = 12;
[BackgroundDependencyLoader]
private void load(DrawableHitObject hitObject)
{
Masking = true;
Origin = Anchor.Centre;
- Size = new Vector2(size);
+ Size = new Vector2(SIZE);
BorderThickness = 3;
BorderColour = Color4.White;
Child = new Box
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs
index 0f9c97059c..ec63e1194d 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs
@@ -19,11 +19,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
switch (lookup)
{
case GameplaySkinComponentLookup resultComponent:
+ HitResult result = resultComponent.Component;
+
// This should eventually be moved to a skin setting, when supported.
- if (Skin is ArgonProSkin && (resultComponent.Component == HitResult.Great || resultComponent.Component == HitResult.Perfect))
+ if (Skin is ArgonProSkin && (result == HitResult.Great || result == HitResult.Perfect))
return Drawable.Empty();
- return new ArgonJudgementPiece(resultComponent.Component);
+ switch (result)
+ {
+ case HitResult.IgnoreMiss:
+ case HitResult.LargeTickMiss:
+ return new ArgonJudgementPieceSliderTickMiss(result);
+
+ default:
+ return new ArgonJudgementPiece(result);
+ }
case OsuSkinComponentLookup osuComponent:
// TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries.
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultJudgementPieceSliderTickMiss.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultJudgementPieceSliderTickMiss.cs
new file mode 100644
index 0000000000..04c15a1433
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultJudgementPieceSliderTickMiss.cs
@@ -0,0 +1,52 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.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();
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs
new file mode 100644
index 0000000000..7a4c768aa2
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Default
+{
+ public class OsuTrianglesSkinTransformer : SkinTransformer
+ {
+ public OsuTrianglesSkinTransformer(ISkin skin)
+ : base(skin)
+ {
+ }
+
+ public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
+ {
+ switch (lookup)
+ {
+ case GameplaySkinComponentLookup 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);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs
index 3c6319ddf9..900d6523cf 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs
@@ -30,8 +30,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Size = new Vector2(200),
- Child = new InputDrum()
+ Size = new Vector2(180f, 200f),
+ Child = new InputDrum
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
}
});
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
index c89e2b727b..d1a8a048ed 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
@@ -1,17 +1,19 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
+using osuTK;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
@@ -37,11 +39,14 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Beatmap.Value.Track.Start();
});
- AddStep("Load playfield", () => SetContents(_ => new TaikoPlayfield
+ AddStep("Load playfield", () => SetContents(_ => new Container
{
- Height = 0.2f,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(2f, 1f),
+ Scale = new Vector2(0.5f),
+ Child = new TaikoPlayfieldAdjustmentContainer { Child = new TaikoPlayfield() },
}));
}
@@ -54,7 +59,20 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[Test]
public void TestHeightChanges()
{
- AddRepeatStep("change height", () => this.ChildrenOfType().ForEach(p => p.Height = Math.Max(0.2f, (p.Height + 0.2f) % 1f)), 50);
+ int value = 0;
+
+ AddRepeatStep("change height", () =>
+ {
+ value = (value + 1) % 5;
+
+ this.ChildrenOfType().ForEach(p =>
+ {
+ var parent = (Container)p.Parent.AsNonNull();
+ parent.Scale = new Vector2(0.5f + 0.1f * value);
+ parent.Width = 1f / parent.Scale.X;
+ parent.Height = 0.5f / parent.Scale.Y;
+ });
+ }, 50);
}
[Test]
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
index 8b1a4f688c..329fff5b42 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
{
InternalChild = piece = new HitPiece
{
- Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT)
+ Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.BASE_HEIGHT)
};
}
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
index b0919417a4..cd52398086 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
@@ -39,15 +39,15 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
{
headPiece = new HitPiece
{
- Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT)
+ Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.BASE_HEIGHT)
},
lengthPiece = new LengthPiece
{
- Height = TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT
+ Height = TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.BASE_HEIGHT
},
tailPiece = new HitPiece
{
- Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT)
+ Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.BASE_HEIGHT)
}
};
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
index cdeaafde10..f63d6c2673 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
@@ -16,9 +16,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldAspectRange.Value = false;
-
- var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
- playfield.ClassicHitTargetPosition.Value = true;
}
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
index 733772e21f..64f2f4c18a 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
@@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
///
private Vector2 adjustSizeForPlayfieldAspectRatio(float size)
{
- return new Vector2(0, size * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
+ return new Vector2(0, size * taikoPlayfield.Parent!.Scale.Y);
}
protected override void UpdateFlashlightSize(float size)
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonInputDrum.cs
index f7b7105bdc..f22c7bf017 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonInputDrum.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
public ArgonInputDrum()
{
- RelativeSizeAxes = Axes.Y;
+ RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs
index bbd62ff85b..724e387cc7 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs
@@ -17,7 +17,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{
- public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
+ public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement
{
private RingExplosion? ringExplosion;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultInputDrum.cs
index 60bacf6413..3eb4f6b8a6 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultInputDrum.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
{
public DefaultInputDrum()
{
- RelativeSizeAxes = Axes.Y;
+ RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
index 28415bb72a..838f172186 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Skinning;
using osuTK;
@@ -17,22 +18,20 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
///
internal partial class LegacyInputDrum : Container
{
- private Container content = null!;
private LegacyHalfDrum left = null!;
private LegacyHalfDrum right = null!;
public LegacyInputDrum()
{
- RelativeSizeAxes = Axes.Y;
- AutoSizeAxes = Axes.X;
+ RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
- Child = content = new Container
+ Child = new Container
{
- Size = new Vector2(180, 200),
+ RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Sprite
@@ -65,33 +64,24 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
const float ratio = 1.6f;
// because the right half is flipped, we need to position using width - position to get the true "topleft" origin position
- float negativeScaleAdjust = content.Width / ratio;
+ const float negative_scale_adjust = TaikoPlayfield.INPUT_DRUM_WIDTH / ratio;
if (skin.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value >= 2.1m)
{
left.Centre.Position = new Vector2(0, taiko_bar_y) * ratio;
- right.Centre.Position = new Vector2(negativeScaleAdjust - 56, taiko_bar_y) * ratio;
+ right.Centre.Position = new Vector2(negative_scale_adjust - 56, taiko_bar_y) * ratio;
left.Rim.Position = new Vector2(0, taiko_bar_y) * ratio;
- right.Rim.Position = new Vector2(negativeScaleAdjust - 56, taiko_bar_y) * ratio;
+ right.Rim.Position = new Vector2(negative_scale_adjust - 56, taiko_bar_y) * ratio;
}
else
{
left.Centre.Position = new Vector2(18, taiko_bar_y + 31) * ratio;
- right.Centre.Position = new Vector2(negativeScaleAdjust - 54, taiko_bar_y + 31) * ratio;
+ right.Centre.Position = new Vector2(negative_scale_adjust - 54, taiko_bar_y + 31) * ratio;
left.Rim.Position = new Vector2(8, taiko_bar_y + 23) * ratio;
- right.Rim.Position = new Vector2(negativeScaleAdjust - 53, taiko_bar_y + 23) * ratio;
+ right.Rim.Position = new Vector2(negative_scale_adjust - 53, taiko_bar_y + 23) * ratio;
}
}
- protected override void Update()
- {
- base.Update();
-
- // Relying on RelativeSizeAxes.Both + FillMode.Fit doesn't work due to the precise pixel layout requirements.
- // This is a bit ugly but makes the non-legacy implementations a lot cleaner to implement.
- content.Scale = new Vector2(DrawHeight / content.Size.Y);
- }
-
///
/// A half-drum. Contains one centre and one rim hit.
///
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs
index 492782f0d1..0b43f1c845 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs
@@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
-using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Skinning;
using osuTK;
@@ -13,47 +12,30 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
public partial class TaikoLegacyHitTarget : CompositeDrawable
{
- private Container content = null!;
-
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
RelativeSizeAxes = Axes.Both;
- InternalChild = content = new Container
+ InternalChildren = new Drawable[]
{
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Children = new Drawable[]
+ new Sprite
{
- new Sprite
- {
- Texture = skin.GetTexture("approachcircle"),
- Scale = new Vector2(0.83f),
- Alpha = 0.47f, // eyeballed to match stable
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- new Sprite
- {
- Texture = skin.GetTexture("taikobigcircle"),
- Scale = new Vector2(0.8f),
- Alpha = 0.22f, // eyeballed to match stable
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- }
+ Texture = skin.GetTexture("approachcircle"),
+ Scale = new Vector2(0.83f),
+ Alpha = 0.47f, // eyeballed to match stable
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ new Sprite
+ {
+ Texture = skin.GetTexture("taikobigcircle"),
+ Scale = new Vector2(0.8f),
+ Alpha = 0.22f, // eyeballed to match stable
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
};
}
-
- protected override void Update()
- {
- base.Update();
-
- // Relying on RelativeSizeAxes.Both + FillMode.Fit doesn't work due to the precise pixel layout requirements.
- // This is a bit ugly but makes the non-legacy implementations a lot cleaner to implement.
- content.Scale = new Vector2(DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
- }
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index 49b0ad811d..77b2b06c0e 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -16,7 +16,6 @@ using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Rulesets.Timing;
@@ -36,6 +35,8 @@ namespace osu.Game.Rulesets.Taiko.UI
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
+ protected new TaikoPlayfieldAdjustmentContainer PlayfieldAdjustmentContainer => (TaikoPlayfieldAdjustmentContainer)base.PlayfieldAdjustmentContainer;
+
protected override bool UserScrollSpeedAdjustment => false;
private SkinnableDrawable scroller;
@@ -68,22 +69,7 @@ namespace osu.Game.Rulesets.Taiko.UI
TimeRange.Value = ComputeTimeRange();
}
- protected virtual double ComputeTimeRange()
- {
- // Taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
- const float scroll_rate = 10;
-
- // Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
- // Width is used because it defines how many notes fit on the playfield.
- // We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default.
- float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT);
-
- // Stable internally increased the slider velocity of objects by a factor of `VELOCITY_MULTIPLIER`.
- // To simulate this, we shrink the time range by that factor here.
- // This, when combined with the rest of the scrolling ruleset machinery (see `MultiplierControlPoint` et al.),
- // has the effect of increasing each multiplier control point's multiplier by `VELOCITY_MULTIPLIER`, ensuring parity with stable.
- return (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate / TaikoBeatmapConverter.VELOCITY_MULTIPLIER;
- }
+ protected virtual double ComputeTimeRange() => PlayfieldAdjustmentContainer.ComputeTimeRange();
protected override void UpdateAfterChildren()
{
diff --git a/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs b/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs
index 29ccd96675..0b7f6f621a 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs
@@ -179,10 +179,9 @@ namespace osu.Game.Rulesets.Taiko.UI
TaikoAction taikoAction = getTaikoActionFromPosition(position);
// Not too sure how this can happen, but let's avoid throwing.
- if (trackedActions.ContainsKey(source))
+ if (!trackedActions.TryAdd(source, taikoAction))
return;
- trackedActions.Add(source, taikoAction);
keyBindingContainer.TriggerPressed(taikoAction);
}
diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
index 725857ed34..d0a8cf647d 100644
--- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
@@ -14,12 +14,6 @@ namespace osu.Game.Rulesets.Taiko.UI
///
internal partial class InputDrum : Container
{
- public InputDrum()
- {
- AutoSizeAxes = Axes.X;
- RelativeSizeAxes = Axes.Y;
- }
-
[BackgroundDependencyLoader]
private void load()
{
@@ -27,8 +21,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{
new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.InputDrum), _ => new DefaultInputDrum())
{
- RelativeSizeAxes = Axes.Y,
- AutoSizeAxes = Axes.X,
+ RelativeSizeAxes = Axes.Both,
},
};
}
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index 7e3ed7a4d4..0510f08068 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
@@ -21,21 +20,17 @@ using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Scoring;
using osu.Game.Skinning;
-using osuTK;
namespace osu.Game.Rulesets.Taiko.UI
{
public partial class TaikoPlayfield : ScrollingPlayfield
{
///
- /// Default height of a when inside a .
+ /// Base height of a when inside a .
///
- public const float DEFAULT_HEIGHT = 200;
+ public const float BASE_HEIGHT = 200;
- ///
- /// Whether the hit target should be nudged further towards the left area, matching the stable "classic" position.
- ///
- public Bindable ClassicHitTargetPosition = new BindableBool();
+ public const float INPUT_DRUM_WIDTH = 180f;
public Container UnderlayElements { get; private set; } = null!;
@@ -44,14 +39,12 @@ namespace osu.Game.Rulesets.Taiko.UI
private JudgementContainer judgementContainer = null!;
private ScrollingHitObjectContainer drumRollHitContainer = null!;
internal Drawable HitTarget = null!;
- private SkinnableDrawable mascot = null!;
private JudgementPooler judgementPooler = null!;
private readonly IDictionary explosionPools = new Dictionary();
private ProxyContainer topLevelHitContainer = null!;
private InputDrum inputDrum = null!;
- private Container rightArea = null!;
///
/// is purposefully not called on this to prevent i.e. being able to interact
@@ -59,19 +52,18 @@ namespace osu.Game.Rulesets.Taiko.UI
///
private BarLinePlayfield barLinePlayfield = null!;
- private Container barLineContent = null!;
- private Container hitObjectContent = null!;
- private Container overlayContent = null!;
-
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
+ const float hit_target_width = BASE_HEIGHT;
+ const float hit_target_offset = -24f;
+
inputDrum = new InputDrum
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
- AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
+ Width = INPUT_DRUM_WIDTH,
};
InternalChildren = new[]
@@ -80,8 +72,8 @@ namespace osu.Game.Rulesets.Taiko.UI
new Container
{
Name = "Left overlay",
- RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit,
+ RelativeSizeAxes = Axes.Y,
+ Width = INPUT_DRUM_WIDTH,
BorderColour = colours.Gray0,
Children = new[]
{
@@ -89,7 +81,7 @@ namespace osu.Game.Rulesets.Taiko.UI
inputDrum.CreateProxy(),
}
},
- mascot = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Mascot), _ => Empty())
+ new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Mascot), _ => Empty())
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.TopLeft,
@@ -97,18 +89,19 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.None,
Y = 0.2f
},
- rightArea = new Container
+ new Container
{
Name = "Right area",
RelativeSizeAxes = Axes.Both,
- RelativePositionAxes = Axes.Both,
+ Padding = new MarginPadding { Left = INPUT_DRUM_WIDTH },
Children = new Drawable[]
{
new Container
{
- Name = "Elements before hit objects",
- RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit,
+ Name = "Elements behind hit objects",
+ RelativeSizeAxes = Axes.Y,
+ Width = hit_target_width,
+ X = hit_target_offset,
Children = new[]
{
new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.KiaiGlow), _ => Empty())
@@ -125,10 +118,11 @@ namespace osu.Game.Rulesets.Taiko.UI
}
}
},
- barLineContent = new Container
+ new Container
{
Name = "Bar line content",
RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Left = hit_target_width / 2 + hit_target_offset },
Children = new Drawable[]
{
UnderlayElements = new Container
@@ -138,17 +132,19 @@ namespace osu.Game.Rulesets.Taiko.UI
barLinePlayfield = new BarLinePlayfield(),
}
},
- hitObjectContent = new Container
+ new Container
{
Name = "Masked hit objects content",
RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Left = hit_target_width / 2 + hit_target_offset },
Masking = true,
Child = HitObjectContainer,
},
- overlayContent = new Container
+ new Container
{
- Name = "Elements after hit objects",
+ Name = "Overlay content",
RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Left = hit_target_width / 2 + hit_target_offset },
Children = new Drawable[]
{
drumRollHitContainer = new DrumRollHitContainer(),
@@ -222,20 +218,6 @@ namespace osu.Game.Rulesets.Taiko.UI
topLevelHitContainer.Add(taikoObject.CreateProxiedContent());
}
- protected override void Update()
- {
- base.Update();
-
- // Padding is required to be updated for elements which are based on "absolute" X sized elements.
- // This is basically allowing for correct alignment as relative pieces move around them.
- rightArea.Padding = new MarginPadding { Left = inputDrum.Width };
- barLineContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 };
- hitObjectContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 };
- overlayContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 };
-
- mascot.Scale = new Vector2(DrawHeight / DEFAULT_HEIGHT);
- }
-
#region Pooling support
public override void Add(HitObject h)
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
index 54608b77de..c67f61052c 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
@@ -4,24 +4,38 @@
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.UI;
+using osuTK;
namespace osu.Game.Rulesets.Taiko.UI
{
public partial class TaikoPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{
- private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
-
public const float MAXIMUM_ASPECT = 16f / 9f;
public const float MINIMUM_ASPECT = 5f / 4f;
+ private const float stable_gamefield_height = 480f;
+
public readonly IBindable LockPlayfieldAspectRange = new BindableBool(true);
+ public TaikoPlayfieldAdjustmentContainer()
+ {
+ RelativeSizeAxes = Axes.X;
+ RelativePositionAxes = Axes.Y;
+ Height = TaikoPlayfield.BASE_HEIGHT;
+
+ // Matches stable, see https://github.com/peppy/osu-stable-reference/blob/7519cafd1823f1879c0d9c991ba0e5c7fd3bfa02/osu!/GameModes/Play/Rulesets/Taiko/RulesetTaiko.cs#L514
+ Y = 135f / stable_gamefield_height;
+ }
+
protected override void Update()
{
base.Update();
- float height = default_relative_height;
+ const float base_relative_height = TaikoPlayfield.BASE_HEIGHT / 768;
+
+ float relativeHeight = base_relative_height;
// Players coming from stable expect to be able to change the aspect ratio regardless of the window size.
// We originally wanted to limit this more, but there was considerable pushback from the community.
@@ -33,19 +47,46 @@ namespace osu.Game.Rulesets.Taiko.UI
float currentAspect = Parent!.ChildSize.X / Parent!.ChildSize.Y;
if (currentAspect > MAXIMUM_ASPECT)
- height *= currentAspect / MAXIMUM_ASPECT;
+ relativeHeight *= currentAspect / MAXIMUM_ASPECT;
else if (currentAspect < MINIMUM_ASPECT)
- height *= currentAspect / MINIMUM_ASPECT;
+ relativeHeight *= currentAspect / MINIMUM_ASPECT;
}
// Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions.
- height = Math.Min(height, 1f / 3f);
- Height = height;
+ relativeHeight = Math.Min(relativeHeight, 1f / 3f);
- // Position the taiko playfield exactly one playfield from the top of the screen, if there is enough space for it.
- // Note that the relative height cannot exceed one-third - if that limit is hit, the playfield will be exactly centered.
- RelativePositionAxes = Axes.Y;
- Y = height;
+ Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f));
+ Width = 1 / Scale.X;
+ }
+
+ public double ComputeTimeRange()
+ {
+ float currentAspect = Parent!.ChildSize.X / Parent!.ChildSize.Y;
+
+ if (LockPlayfieldAspectRange.Value)
+ currentAspect = Math.Clamp(currentAspect, MINIMUM_ASPECT, MAXIMUM_ASPECT);
+
+ // in a game resolution of 1024x768, stable's scrolling system consists of objects being placed 600px (widthScaled - 40) away from their hit location.
+ // however, the point at which the object renders at the end of the screen is exactly x=640, but stable makes the object start moving from beyond the screen instead of the boundary point.
+ // therefore, in lazer we have to adjust the "in length" so that it's in a 640px->160px fashion before passing it down as a "time range".
+ // see stable's "in length": https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManagerTaiko.cs#L168
+ const float stable_hit_location = 160f;
+ float widthScaled = currentAspect * stable_gamefield_height;
+ float inLength = widthScaled - stable_hit_location;
+
+ // also in a game resolution of 1024x768, stable makes hit objects scroll from 760px->160px at a duration of 6000ms, divided by slider velocity (i.e. at a rate of 0.1px/ms)
+ // compare: https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManagerTaiko.cs#L218
+ // note: the variable "sv", in the linked reference, is equivalent to MultiplierControlPoint.Multiplier * 100, but since time range is agnostic of velocity, we replace "sv" with 100 below.
+ float inMsLength = inLength / 100 * 1000;
+
+ // stable multiplies the slider velocity by 1.4x for certain reasons, divide the time range by that factor to achieve similar result.
+ // for references on how the factor is applied to the time range, see:
+ // 1. https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManagerTaiko.cs#L79 (DifficultySliderMultiplier multiplied by 1.4x)
+ // 2. https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManager.cs#L468-L470 (DifficultySliderMultiplier used to calculate SliderScoringPointDistance)
+ // 3. https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManager.cs#L248-L250 (SliderScoringPointDistance used to calculate slider velocity, i.e. the "sv" variable from above)
+ inMsLength /= TaikoBeatmapConverter.VELOCITY_MULTIPLIER;
+
+ return inMsLength;
}
}
}
diff --git a/osu.Game.Tests/Beatmaps/WorkingBeatmapTest.cs b/osu.Game.Tests/Beatmaps/WorkingBeatmapTest.cs
index f4b1028c0e..3c26f8e39a 100644
--- a/osu.Game.Tests/Beatmaps/WorkingBeatmapTest.cs
+++ b/osu.Game.Tests/Beatmaps/WorkingBeatmapTest.cs
@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using JetBrains.Annotations;
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
@@ -98,9 +99,10 @@ namespace osu.Game.Tests.Beatmaps
Beatmap = beatmap;
}
+#pragma warning disable CS0067
+ [CanBeNull]
public event Action> ObjectConverted;
-
- protected virtual void OnObjectConverted(HitObject arg1, IEnumerable arg2) => ObjectConverted?.Invoke(arg1, arg2);
+#pragma warning restore CS0067
public IBeatmap Beatmap { get; }
diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs
index eae12edebd..95fd2669e5 100644
--- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs
+++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs
@@ -75,8 +75,6 @@ namespace osu.Game.Tests.Chat
return false;
};
});
-
- AddUntilStep("wait for notifications client", () => channelManager.NotificationsConnected);
}
[Test]
diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs
index 8b4c6e2411..ddf207342a 100644
--- a/osu.Game.Tests/Database/RulesetStoreTests.cs
+++ b/osu.Game.Tests/Database/RulesetStoreTests.cs
@@ -79,7 +79,7 @@ namespace osu.Game.Tests.Database
Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True);
// Availability is updated on construction of a RealmRulesetStore
- var _ = new RealmRulesetStore(realm, storage);
+ _ = new RealmRulesetStore(realm, storage);
Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.False);
});
@@ -104,13 +104,13 @@ namespace osu.Game.Tests.Database
Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True);
// Availability is updated on construction of a RealmRulesetStore
- var _ = new RealmRulesetStore(realm, storage);
+ _ = new RealmRulesetStore(realm, storage);
Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.False);
// Simulate the ruleset getting updated
LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION;
- var __ = new RealmRulesetStore(realm, storage);
+ _ = new RealmRulesetStore(realm, storage);
Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True);
});
diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
index 4fb9db845b..61161f3206 100644
--- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
@@ -203,9 +203,9 @@ namespace osu.Game.Tests.Gameplay
public IRenderer Renderer => host.Renderer;
public AudioManager AudioManager => Audio;
- public IResourceStore Files => null;
+ public IResourceStore Files => null!;
public new IResourceStore Resources => base.Resources;
- public RealmAccess RealmAccess => null;
+ public RealmAccess RealmAccess => null!;
public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null;
#endregion
diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
index fbe5a0e4d7..98cb66a234 100644
--- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
+++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs
@@ -169,9 +169,9 @@ namespace osu.Game.Tests.NonVisual.Skinning
public IRenderer Renderer => new DummyRenderer();
public AudioManager AudioManager => null;
- public IResourceStore Files => null;
- public IResourceStore Resources => null;
- public RealmAccess RealmAccess => null;
+ public IResourceStore Files => null!;
+ public IResourceStore Resources => null!;
+ public RealmAccess RealmAccess => null!;
public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => textureStore;
}
}
diff --git a/osu.Game.Tests/Rulesets/TestSceneBrokenRulesetHandling.cs b/osu.Game.Tests/Rulesets/TestSceneBrokenRulesetHandling.cs
index dac6beea65..b378704e80 100644
--- a/osu.Game.Tests/Rulesets/TestSceneBrokenRulesetHandling.cs
+++ b/osu.Game.Tests/Rulesets/TestSceneBrokenRulesetHandling.cs
@@ -56,9 +56,9 @@ namespace osu.Game.Tests.Rulesets
public override IEnumerable GetModsFor(ModType type) => new Mod[] { null };
- public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => null;
- public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null;
- public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null;
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => null!;
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!;
+ public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!;
}
private class TestAPIIncompatibleRuleset : Ruleset
@@ -69,9 +69,9 @@ namespace osu.Game.Tests.Rulesets
// simulate API incompatibility by throwing similar exceptions.
public override IEnumerable GetModsFor(ModType type) => throw new MissingMethodException();
- public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => null;
- public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null;
- public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null;
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => null!;
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!;
+ public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!;
}
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
index f2b3351533..6c36e6729e 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
@@ -210,6 +210,13 @@ namespace osu.Game.Tests.Visual.Editing
switchPresets(-1);
assertPreset(BeatDivisorType.Custom, 15);
assertBeatSnap(15);
+
+ setDivisorViaInput(24);
+ assertPreset(BeatDivisorType.Custom, 24);
+ switchPresets(1);
+ assertPreset(BeatDivisorType.Common);
+ switchPresets(-2);
+ assertPreset(BeatDivisorType.Triplets);
}
private void switchBeatSnap(int direction) => AddRepeatStep($"move snap {(direction > 0 ? "forward" : "backward")}", () =>
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
index bbd7123f20..ca5e89c8ed 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
@@ -142,7 +142,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("dismiss prompt", () =>
{
- var button = DialogOverlay.CurrentDialog.Buttons.Last();
+ var button = DialogOverlay.CurrentDialog!.Buttons.Last();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
@@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Editing
});
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
- AddStep("save changes", () => DialogOverlay.CurrentDialog.PerformOkAction());
+ AddStep("save changes", () => DialogOverlay.CurrentDialog!.PerformOkAction());
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
index 3ac4d25028..636cd78d9c 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
@@ -47,8 +47,8 @@ namespace osu.Game.Tests.Visual.Gameplay
seekTo(referenceBeatmap.HitObjects[^1].GetEndTime());
AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true);
- AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100);
- AddAssert("score has no misses", () => getResultsScreen().Score.Statistics[HitResult.Miss] == 0);
+ AddAssert("score has combo", () => getResultsScreen().Score!.Combo > 100);
+ AddAssert("score has no misses", () => getResultsScreen().Score!.Statistics[HitResult.Miss] == 0);
AddUntilStep("avatar displayed", () => getAvatar() != null);
AddAssert("avatar not clickable", () => getAvatar().ChildrenOfType().First().Action == null);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
index fafd1330cc..1660f93384 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
@@ -138,8 +138,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
// Player creates new instance of mods after gameplay to ensure any runtime references to drawables etc. are not retained.
- AddAssert("results screen score has copied mods", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.Not.SameAs(playerMods.First()));
- AddAssert("results screen score has matching", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.EqualTo(playerMods.First()));
+ AddAssert("results screen score has copied mods", () => (Player.GetChildScreen() as ResultsScreen)?.Score?.Mods.First(), () => Is.Not.SameAs(playerMods.First()));
+ AddAssert("results screen score has matching", () => (Player.GetChildScreen() as ResultsScreen)?.Score?.Mods.First(), () => Is.EqualTo(playerMods.First()));
AddUntilStep("score in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null));
AddUntilStep("databased score has correct mods", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID))!.Mods.First(), () => Is.EqualTo(playerMods.First()));
@@ -184,7 +184,11 @@ namespace osu.Game.Tests.Visual.Gameplay
CreateTest();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
- AddStep("log back in", () => API.Login("username", "password"));
+ AddStep("log back in", () =>
+ {
+ API.Login("username", "password");
+ ((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
+ });
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
index c51883b221..a7ab021884 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
@@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void Update()
{
base.Update();
- playbackManager?.ReplayInputHandler.SetFrameFromTime(Time.Current - 100);
+ playbackManager?.ReplayInputHandler?.SetFrameFromTime(Time.Current - 100);
}
[TearDownSteps]
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
index 3a5b3864af..dd5bbf70b4 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
@@ -198,7 +198,7 @@ namespace osu.Game.Tests.Visual.Gameplay
foreach (var legacyFrame in frames.Frames)
{
var frame = new TestReplayFrame();
- frame.FromLegacy(legacyFrame, null);
+ frame.FromLegacy(legacyFrame, null!);
playbackReplay.Frames.Add(frame);
}
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs
index 0bc71924ce..5fc075ed99 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
+using System.Net;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -9,7 +10,9 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
using osu.Game.Overlays;
+using osu.Game.Overlays.Login;
using osu.Game.Users.Drawables;
using osuTK.Input;
@@ -18,6 +21,8 @@ namespace osu.Game.Tests.Visual.Menus
[TestFixture]
public partial class TestSceneLoginOverlay : OsuManualInputManagerTestScene
{
+ private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
+
private LoginOverlay loginOverlay = null!;
[BackgroundDependencyLoader]
@@ -40,9 +45,69 @@ namespace osu.Game.Tests.Visual.Menus
public void TestLoginSuccess()
{
AddStep("logout", () => API.Logout());
+ assertAPIState(APIState.Offline);
AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password");
AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick());
+
+ assertAPIState(APIState.RequiresSecondFactorAuth);
+ AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType().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().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().First().Text = "password");
+ AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick());
+
+ assertAPIState(APIState.RequiresSecondFactorAuth);
+ AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType().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().First().Text = "abcdefgh");
+ AddUntilStep("wait for verification handled", () => verificationHandled);
+ assertAPIState(APIState.RequiresSecondFactorAuth);
+ AddStep("clear handler", () => dummyAPI.HandleRequest = null);
}
[Test]
@@ -78,6 +143,12 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("enter password", () => loginOverlay.ChildrenOfType().First().Text = "password");
AddStep("submit", () => loginOverlay.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick());
+ assertAPIState(APIState.RequiresSecondFactorAuth);
+ AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType().SingleOrDefault(), () => Is.Not.Null);
+
+ AddStep("enter code", () => loginOverlay.ChildrenOfType().First().Text = "88800088");
+ assertAPIState(APIState.Online);
+
AddStep("click on flag", () =>
{
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType().First());
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
index ce9f80a84f..12d7dde11b 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
@@ -250,7 +250,7 @@ namespace osu.Game.Tests.Visual.Menus
{
}
- public virtual IBindable UnreadCount => null;
+ public virtual IBindable UnreadCount { get; } = new Bindable();
public IEnumerable AllNotifications => Enumerable.Empty();
}
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs
index 2bdfc8959d..f0506ed35c 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs
@@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual.Menus
[TestFixture]
public partial class TestSceneToolbarUserButton : OsuManualInputManagerTestScene
{
+ private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
+
public TestSceneToolbarUserButton()
{
Container mainContainer;
@@ -69,18 +71,20 @@ namespace osu.Game.Tests.Visual.Menus
[Test]
public void TestLoginLogout()
{
- AddStep("Log out", () => ((DummyAPIAccess)API).Logout());
- AddStep("Log in", () => ((DummyAPIAccess)API).Login("wang", "jang"));
+ AddStep("Log out", () => dummyAPI.Logout());
+ AddStep("Log in", () => dummyAPI.Login("wang", "jang"));
+ AddStep("Authenticate via second factor", () => dummyAPI.AuthenticateSecondFactor("abcdefgh"));
}
[Test]
public void TestStates()
{
- AddStep("Log in", () => ((DummyAPIAccess)API).Login("wang", "jang"));
+ AddStep("Log in", () => dummyAPI.Login("wang", "jang"));
+ AddStep("Authenticate via second factor", () => dummyAPI.AuthenticateSecondFactor("abcdefgh"));
foreach (var state in Enum.GetValues())
{
- AddStep($"Change state to {state}", () => ((DummyAPIAccess)API).SetState(state));
+ AddStep($"Change state to {state}", () => dummyAPI.SetState(state));
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
index d99d764449..b938e59d63 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
@@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
.SkipWhile(r => r.Room.Category.Value == RoomCategory.Spotlight)
.All(r => r.Room.Category.Value == RoomCategory.Normal));
- AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.FirstOrDefault(r => r.RoomID.Value == 0)));
+ AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.First(r => r.RoomID.Value == 0)));
AddAssert("has 4 rooms", () => container.Rooms.Count == 4);
AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0));
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index 747805edc8..8c7576ff52 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -698,7 +698,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
var scoreInfo = ((ResultsScreen)multiplayerComponents.CurrentScreen).Score;
- return !scoreInfo.Passed && scoreInfo.Rank == ScoreRank.F;
+ return scoreInfo?.Passed == false && scoreInfo.Rank == ScoreRank.F;
});
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
index cbeff770c9..aaf85dab7c 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
@@ -3,13 +3,18 @@
#nullable disable
+using System;
+using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;
@@ -19,14 +24,37 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
private MultiplayerPlayer player;
- [SetUpSteps]
- public override void SetUpSteps()
+ [Test]
+ public void TestGameplay()
{
- base.SetUpSteps();
+ setup();
+ AddUntilStep("wait for gameplay start", () => player.LocalUserPlaying.Value);
+ }
+
+ [Test]
+ public void TestFail()
+ {
+ setup(() => new[] { new OsuModAutopilot() });
+
+ AddUntilStep("wait for gameplay start", () => player.LocalUserPlaying.Value);
+ AddStep("set health zero", () => player.ChildrenOfType().Single().Health.Value = 0);
+ AddUntilStep("wait for fail", () => player.ChildrenOfType().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().First().TriggerPressed(OsuAction.LeftButton));
+ AddUntilStep("score changed", () => player.GameplayState.ScoreProcessor.TotalScore.Value > 0);
+ }
+
+ private void setup(Func> mods = null)
+ {
AddStep("set beatmap", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+ SelectedMods.Value = mods?.Invoke() ?? Array.Empty();
});
AddStep("Start track playing", () =>
@@ -52,11 +80,5 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("gameplay clock is not paused", () => !player.ChildrenOfType().Single().IsPaused.Value);
AddAssert("gameplay clock is running", () => player.ChildrenOfType().Single().IsRunning);
}
-
- [Test]
- public void TestGameplay()
- {
- AddUntilStep("wait for gameplay start", () => player.LocalUserPlaying.Value);
- }
}
}
diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
index 459a8b0df5..6590339311 100644
--- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
@@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual.Navigation
case ScorePresentType.Results:
AddUntilStep("wait for results", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ResultsScreen);
AddStep("store last waited screen", () => lastWaitedScreen = Game.ScreenStack.CurrentScreen);
- AddUntilStep("correct score displayed", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score.Equals(getImport()));
+ AddUntilStep("correct score displayed", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score!.Equals(getImport()));
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.Equals(getImport().Ruleset));
break;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs
index 0f920643f0..b9d7312233 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs
@@ -10,6 +10,8 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.AccountCreation;
@@ -59,7 +61,40 @@ namespace osu.Game.Tests.Visual.Online
AddStep("click button", () => accountCreation.ChildrenOfType().Single().TriggerClick());
AddUntilStep("warning screen is present", () => accountCreation.ChildrenOfType().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().Single().TriggerClick());
+ AddUntilStep("warning screen is present", () => accountCreation.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
+
+ AddStep("proceed", () => accountCreation.ChildrenOfType().Single().TriggerClick());
+ AddUntilStep("entry screen is present", () => accountCreation.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
+
+ AddStep("input details", () =>
+ {
+ var entryScreen = accountCreation.ChildrenOfType().Single();
+ entryScreen.ChildrenOfType().ElementAt(0).Text = "new_user";
+ entryScreen.ChildrenOfType().ElementAt(1).Text = "new.user@fake.mail";
+ entryScreen.ChildrenOfType().ElementAt(2).Text = "password";
+ });
+ AddStep("click button", () => accountCreation.ChildrenOfType().Single()
+ .ChildrenOfType().Single().TriggerClick());
+ AddUntilStep("verification screen is present", () => accountCreation.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
+
+ AddStep("verify", () => ((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh"));
AddUntilStep("overlay is hidden", () => accountCreation.State.Value == Visibility.Hidden);
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
index 10fdffb8e1..f47322b9e0 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
@@ -67,6 +67,7 @@ namespace osu.Game.Tests.Visual.Online
Schedule(() =>
{
API.Login("test", "test");
+ dummyAPI.AuthenticateSecondFactor("abcdefgh");
Child = commentsContainer = new CommentsContainer();
});
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneFavouriteButton.cs b/osu.Game.Tests/Visual/Online/TestSceneFavouriteButton.cs
index 3954fd5cff..0acf8336e3 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneFavouriteButton.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneFavouriteButton.cs
@@ -6,6 +6,7 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
+using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet.Buttons;
using osuTK;
@@ -34,14 +35,22 @@ namespace osu.Game.Tests.Visual.Online
AddStep("set valid beatmap", () => favourite.BeatmapSet.Value = new APIBeatmapSet { OnlineID = 88 });
AddStep("log out", () => API.Logout());
checkEnabled(false);
- AddStep("log in", () => API.Login("test", "test"));
+ AddStep("log in", () =>
+ {
+ API.Login("test", "test");
+ ((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
+ });
checkEnabled(true);
}
[Test]
public void TestBeatmapChange()
{
- AddStep("log in", () => API.Login("test", "test"));
+ AddStep("log in", () =>
+ {
+ API.Login("test", "test");
+ ((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
+ });
AddStep("set valid beatmap", () => favourite.BeatmapSet.Value = new APIBeatmapSet { OnlineID = 88 });
checkEnabled(true);
AddStep("set invalid beatmap", () => favourite.BeatmapSet.Value = new APIBeatmapSet());
diff --git a/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs
index be819afa3e..3607b37c7e 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs
@@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.Online
else
{
int userId = int.Parse(getUserRequest.Lookup);
- string rulesetName = getUserRequest.Ruleset.ShortName;
+ string rulesetName = getUserRequest.Ruleset!.ShortName;
var response = new APIUser
{
Id = userId,
@@ -177,7 +177,11 @@ namespace osu.Game.Tests.Visual.Online
AddWaitStep("wait a bit", 5);
AddAssert("update not received", () => update == null);
- AddStep("log in user", () => dummyAPI.Login("user", "password"));
+ AddStep("log in user", () =>
+ {
+ dummyAPI.Login("user", "password");
+ dummyAPI.AuthenticateSecondFactor("abcdefgh");
+ });
}
[Test]
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
index 1375689075..bc8f75d4ce 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
@@ -52,7 +52,11 @@ namespace osu.Game.Tests.Visual.Online
AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
AddToggleStep("toggle visibility", visible => profile.State.Value = visible ? Visibility.Visible : Visibility.Hidden);
AddStep("log out", () => dummyAPI.Logout());
- AddStep("log back in", () => dummyAPI.Login("username", "password"));
+ AddStep("log back in", () =>
+ {
+ dummyAPI.Login("username", "password");
+ dummyAPI.AuthenticateSecondFactor("abcdefgh");
+ });
}
[Test]
@@ -98,7 +102,11 @@ namespace osu.Game.Tests.Visual.Online
});
AddStep("logout", () => dummyAPI.Logout());
AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
- AddStep("login", () => dummyAPI.Login("username", "password"));
+ AddStep("login", () =>
+ {
+ dummyAPI.Login("username", "password");
+ dummyAPI.AuthenticateSecondFactor("abcdefgh");
+ });
AddWaitStep("wait some", 3);
AddStep("complete request", () => pendingRequest.TriggerSuccess(TEST_USER));
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs
index ce1a9ac6a7..488902c417 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs
@@ -11,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Game.Overlays;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Containers;
+using osu.Game.Online.API;
namespace osu.Game.Tests.Visual.Online
{
@@ -72,7 +73,11 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("Login overlay is visible", () => login.State.Value == Visibility.Visible);
}
- private void logIn() => API.Login("localUser", "password");
+ private void logIn()
+ {
+ API.Login("localUser", "password");
+ ((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
+ }
private Comment getUserComment() => new Comment
{
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs
index 1053789b27..9f7b20ad43 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs
@@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual.Playlists
public IBindable InitialRoomsReceived { get; } = new Bindable(true);
- public IBindableList Rooms => null;
+ public IBindableList Rooms => null!;
public void AddOrUpdateRoom(Room room) => throw new NotImplementedException();
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
index e805b03542..25ee20b089 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
@@ -420,7 +420,7 @@ namespace osu.Game.Tests.Visual.Playlists
public new LoadingSpinner RightSpinner => base.RightSpinner;
public new ScorePanelList ScorePanelList => base.ScorePanelList;
- public TestResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
+ public TestResultsScreen([CanBeNull] ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
: base(score, roomId, playlistItem, allowRetry)
{
}
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs
index 2f66309f04..1636a3d4b8 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs
@@ -52,11 +52,11 @@ namespace osu.Game.Tests.Visual.Playlists
[SetUpSteps]
public void SetupSteps()
{
- AddStep("set room", () => SelectedRoom.Value = new Room());
+ AddStep("set room", () => SelectedRoom!.Value = new Room());
importBeatmap();
- AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(SelectedRoom.Value)));
+ AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(SelectedRoom!.Value)));
AddUntilStep("wait for load", () => match.IsCurrentScreen());
}
@@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Playlists
});
});
- AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom.Value.Playlist[0]);
+ AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom!.Value.Playlist[0]);
}
[Test]
@@ -201,7 +201,7 @@ namespace osu.Game.Tests.Visual.Playlists
private void setupAndCreateRoom(Action room)
{
- AddStep("setup room", () => room(SelectedRoom.Value));
+ AddStep("setup room", () => room(SelectedRoom!.Value));
AddStep("click create button", () =>
{
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
index 7aa36429a7..41a5603060 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
@@ -46,6 +46,16 @@ namespace osu.Game.Tests.Visual.Ranking
addCircleStep(createScore(1, new OsuRuleset()));
}
+ [Test]
+ public void TestOsuRankHidden()
+ {
+ addCircleStep(createScore(0, new OsuRuleset(), 20, true));
+ addCircleStep(createScore(0.8, new OsuRuleset(), 5, true));
+ addCircleStep(createScore(0.95, new OsuRuleset(), 0, true));
+ addCircleStep(createScore(0.97, new OsuRuleset(), 1, true));
+ addCircleStep(createScore(1, new OsuRuleset(), 0, true));
+ }
+
[Test]
public void TestCatchRank()
{
@@ -66,7 +76,7 @@ namespace osu.Game.Tests.Visual.Ranking
addCircleStep(createScore(1, new CatchRuleset()));
}
- private void addCircleStep(ScoreInfo score) => AddStep($"add panel ({score.DisplayAccuracy})", () =>
+ private void addCircleStep(ScoreInfo score) => AddStep($"add panel ({score.DisplayAccuracy}, {score.Statistics.GetValueOrDefault(HitResult.Miss)} miss)", () =>
{
Children = new Drawable[]
{
@@ -93,18 +103,22 @@ namespace osu.Game.Tests.Visual.Ranking
};
});
- private ScoreInfo createScore(double accuracy, Ruleset ruleset)
+ private ScoreInfo createScore(double accuracy, Ruleset ruleset, int missCount = 0, bool hidden = false)
{
var scoreProcessor = ruleset.CreateScoreProcessor();
var statistics = new Dictionary
{
- { HitResult.Miss, 1 },
+ { HitResult.Miss, missCount },
{ HitResult.Meh, 50 },
{ HitResult.Good, 100 },
{ HitResult.Great, 300 },
};
+ var mods = hidden
+ ? new[] { new OsuModHidden() }
+ : new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() };
+
return new ScoreInfo
{
User = new APIUser
@@ -114,7 +128,7 @@ namespace osu.Game.Tests.Visual.Ranking
},
BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo,
Ruleset = ruleset.RulesetInfo,
- Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
+ Mods = mods,
TotalScore = 2845370,
Accuracy = accuracy,
MaxCombo = 999,
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
index 866e20d063..685a685896 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
@@ -418,7 +418,7 @@ namespace osu.Game.Tests.Visual.Ranking
public UnrankedSoloResultsScreen(ScoreInfo score)
: base(score, true)
{
- Score.BeatmapInfo!.OnlineID = 0;
+ Score!.BeatmapInfo!.OnlineID = 0;
Score.BeatmapInfo.Status = BeatmapOnlineStatus.Pending;
}
@@ -432,7 +432,7 @@ namespace osu.Game.Tests.Visual.Ranking
private class RulesetWithNoPerformanceCalculator : OsuRuleset
{
- public override PerformanceCalculator CreatePerformanceCalculator() => null;
+ public override PerformanceCalculator CreatePerformanceCalculator() => null!;
}
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs
index b17024ae8f..e1d40882be 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs
@@ -125,7 +125,11 @@ namespace osu.Game.Tests.Visual.UserInterface
assertLoggedOutState();
// moving from logged out -> logged in
- AddStep("log back in", () => dummyAPI.Login("username", "password"));
+ AddStep("log back in", () =>
+ {
+ dummyAPI.Login("username", "password");
+ dummyAPI.AuthenticateSecondFactor("abcdefgh");
+ });
assertLoggedInState();
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs
index 9275f9755f..51da4d8755 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs
@@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
}
- public virtual IBindable UnreadCount => null;
+ public virtual IBindable UnreadCount { get; } = new Bindable();
public IEnumerable AllNotifications => Enumerable.Empty();
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs
index a95bb2c9e3..b79ce6c75f 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
@@ -67,6 +68,15 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert(@"Check empty multiplier", () => assertModsMultiplier(Array.Empty()));
}
+ [Test]
+ public void TestUnrankedBadge()
+ {
+ AddStep(@"Add unranked mod", () => changeMods(new[] { new OsuModDeflate() }));
+ AddAssert("Unranked badge shown", () => footerButtonMods.UnrankedBadge.Alpha == 1);
+ AddStep(@"Clear selected mod", () => changeMods(Array.Empty()));
+ AddAssert("Unranked badge not shown", () => footerButtonMods.UnrankedBadge.Alpha == 0);
+ }
+
private void changeMods(IReadOnlyList mods)
{
footerButtonMods.Current.Value = mods;
@@ -83,6 +93,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private partial class TestFooterButtonMods : FooterButtonMods
{
public new OsuSpriteText MultiplierText => base.MultiplierText;
+ public new Drawable UnrankedBadge => base.UnrankedBadge;
}
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index 046954db47..99a5897dff 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -119,7 +119,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod multiplier correct", () =>
{
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
- return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType().Single().Current.Value);
+ return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value);
});
assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType().Any());
@@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod multiplier correct", () =>
{
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
- return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType().Single().Current.Value);
+ return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value);
});
assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType().Any());
@@ -846,7 +846,7 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.Click(MouseButton.Left);
});
AddAssert("difficulty multiplier display shows correct value",
- () => modSelectOverlay.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(0.1).Within(Precision.DOUBLE_EPSILON));
+ () => modSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(0.1).Within(Precision.DOUBLE_EPSILON));
// this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation,
// it is instrumental in the reproduction of the failure scenario that this test is supposed to cover.
@@ -856,7 +856,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType().Single()
.ChildrenOfType>().Single().TriggerClick());
AddUntilStep("difficulty multiplier display shows correct value",
- () => modSelectOverlay.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON));
+ () => modSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON));
}
private void waitForColumnLoad() => AddUntilStep("all column content loaded", () =>
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScoreMultiplierDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRankingInformationDisplay.cs
similarity index 50%
rename from osu.Game.Tests/Visual/UserInterface/TestSceneScoreMultiplierDisplay.cs
rename to osu.Game.Tests/Visual/UserInterface/TestSceneRankingInformationDisplay.cs
index c2ddd814b7..42f243cc21 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneScoreMultiplierDisplay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRankingInformationDisplay.cs
@@ -11,7 +11,7 @@ using osu.Game.Overlays.Mods;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
- public partial class TestSceneScoreMultiplierDisplay : OsuTestScene
+ public partial class TestSceneRankingInformationDisplay : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
@@ -19,22 +19,24 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestBasic()
{
- ScoreMultiplierDisplay multiplierDisplay = null!;
+ RankingInformationDisplay onlinePropertiesDisplay = null!;
- AddStep("create content", () => Child = multiplierDisplay = new ScoreMultiplierDisplay
+ AddStep("create content", () => Child = onlinePropertiesDisplay = new RankingInformationDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
- AddStep("set multiplier below 1", () => multiplierDisplay.Current.Value = 0.5);
- AddStep("set multiplier to 1", () => multiplierDisplay.Current.Value = 1);
- AddStep("set multiplier above 1", () => multiplierDisplay.Current.Value = 1.5);
+ AddToggleStep("toggle ranked", ranked => onlinePropertiesDisplay.Ranked.Value = ranked);
+
+ AddStep("set multiplier below 1", () => onlinePropertiesDisplay.ModMultiplier.Value = 0.5);
+ AddStep("set multiplier to 1", () => onlinePropertiesDisplay.ModMultiplier.Value = 1);
+ AddStep("set multiplier above 1", () => onlinePropertiesDisplay.ModMultiplier.Value = 1.5);
AddSliderStep("set multiplier", 0, 2, 1d, multiplier =>
{
- if (multiplierDisplay.IsNotNull())
- multiplierDisplay.Current.Value = multiplier;
+ if (onlinePropertiesDisplay.IsNotNull())
+ onlinePropertiesDisplay.ModMultiplier.Value = multiplier;
});
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
index 7edf892f35..0138ac7569 100644
--- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
@@ -21,7 +21,6 @@ namespace osu.Game.Beatmaps.ControlPoints
///
public readonly BindableDouble ScrollSpeedBindable = new BindableDouble(1)
{
- Precision = 0.01,
MinValue = 0.01,
MaxValue = 10
};
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index 1ee4670ae2..386dada328 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -567,10 +567,9 @@ namespace osu.Game.Beatmaps.Formats
for (int i = pendingControlPoints.Count - 1; i >= 0; i--)
{
var type = pendingControlPoints[i].GetType();
- if (pendingControlPointTypes.Contains(type))
+ if (!pendingControlPointTypes.Add(type))
continue;
- pendingControlPointTypes.Add(type);
beatmap.ControlPointInfo.Add(pendingControlPointsTime, pendingControlPoints[i]);
}
diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
index 2c500146c5..74a85cde7c 100644
--- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
@@ -116,7 +116,7 @@ namespace osu.Game.Beatmaps
ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer();
AudioManager IStorageResourceProvider.AudioManager => audioManager;
- RealmAccess IStorageResourceProvider.RealmAccess => null;
+ RealmAccess IStorageResourceProvider.RealmAccess => null!;
IResourceStore IStorageResourceProvider.Files => files;
IResourceStore IStorageResourceProvider.Resources => resources;
IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore);
diff --git a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs
index 706b05f5ad..4143a6d76d 100644
--- a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs
+++ b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs
@@ -1,17 +1,17 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Utils;
-using osuTK;
using System;
-using osu.Framework.Graphics.Shaders;
-using osu.Framework.Graphics.Textures;
-using osu.Framework.Graphics.Primitives;
-using osu.Framework.Allocation;
using System.Collections.Generic;
-using osu.Framework.Graphics.Rendering;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Rendering;
+using osu.Framework.Graphics.Shaders;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Utils;
+using osuTK;
namespace osu.Game.Graphics.Backgrounds
{
@@ -27,6 +27,8 @@ namespace osu.Game.Graphics.Backgrounds
public float Thickness { get; set; } = 0.02f; // No need for invalidation since it's happening in Update()
+ public float ScaleAdjust { get; set; } = 1;
+
///
/// Whether we should create new triangles as others expire.
///
@@ -106,7 +108,7 @@ namespace osu.Game.Graphics.Backgrounds
parts[i] = newParticle;
- float bottomPos = parts[i].Position.Y + triangle_size * equilateral_triangle_ratio / DrawHeight;
+ float bottomPos = parts[i].Position.Y + triangle_size * ScaleAdjust * equilateral_triangle_ratio / DrawHeight;
if (bottomPos < 0)
parts.RemoveAt(i);
}
@@ -149,7 +151,7 @@ namespace osu.Game.Graphics.Backgrounds
if (randomY)
{
// since triangles are drawn from the top - allow them to be positioned a bit above the screen
- float maxOffset = triangle_size * equilateral_triangle_ratio / DrawHeight;
+ float maxOffset = triangle_size * ScaleAdjust * equilateral_triangle_ratio / DrawHeight;
y = Interpolation.ValueAt(nextRandom(), -maxOffset, 1f, 0f, 1f);
}
@@ -188,7 +190,7 @@ namespace osu.Game.Graphics.Backgrounds
private readonly List parts = new List();
- private readonly Vector2 triangleSize = new Vector2(1f, equilateral_triangle_ratio) * triangle_size;
+ private Vector2 triangleSize;
private Vector2 size;
private float thickness;
@@ -209,6 +211,7 @@ namespace osu.Game.Graphics.Backgrounds
size = Source.DrawSize;
thickness = Source.Thickness;
clampAxes = Source.ClampAxes;
+ triangleSize = new Vector2(1f, equilateral_triangle_ratio) * triangle_size * Source.ScaleAdjust;
Quad triangleQuad = new Quad(
Vector2Extensions.Transform(Vector2.Zero, DrawInfo.Matrix),
diff --git a/osu.Game/Graphics/Containers/SelectionCycleFillFlowContainer.cs b/osu.Game/Graphics/Containers/SelectionCycleFillFlowContainer.cs
index 62544c6111..098fd7b1ab 100644
--- a/osu.Game/Graphics/Containers/SelectionCycleFillFlowContainer.cs
+++ b/osu.Game/Graphics/Containers/SelectionCycleFillFlowContainer.cs
@@ -52,10 +52,10 @@ namespace osu.Game.Graphics.Containers
public override void Add(T drawable)
{
- base.Add(drawable);
-
Debug.Assert(drawable != null);
+ base.Add(drawable);
+
drawable.StateChanged += state => selectionChanged(drawable, state);
}
diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs
index 1b5877b966..985898958c 100644
--- a/osu.Game/Graphics/OsuColour.cs
+++ b/osu.Game/Graphics/OsuColour.cs
@@ -77,7 +77,7 @@ namespace osu.Game.Graphics
{
case HitResult.IgnoreMiss:
case HitResult.SmallTickMiss:
- return Orange1;
+ return Color4.Gray;
case HitResult.Miss:
case HitResult.LargeTickMiss:
diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs
index 26e499ae9a..a085558b3a 100644
--- a/osu.Game/Graphics/ScreenshotManager.cs
+++ b/osu.Game/Graphics/ScreenshotManager.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.IO;
using System.Threading;
@@ -24,6 +22,8 @@ using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
namespace osu.Game.Graphics
{
@@ -37,30 +37,26 @@ namespace osu.Game.Graphics
///
public IBindable CursorVisibility => cursorVisibility;
- private Bindable screenshotFormat;
- private Bindable captureMenuCursor;
+ [Resolved]
+ private GameHost host { get; set; } = null!;
[Resolved]
- private GameHost host { get; set; }
+ private Clipboard clipboard { get; set; } = null!;
[Resolved]
- private Clipboard clipboard { get; set; }
-
- private Storage storage;
+ private INotificationOverlay notificationOverlay { get; set; } = null!;
[Resolved]
- private INotificationOverlay notificationOverlay { get; set; }
+ private OsuConfigManager config { get; set; } = null!;
- private Sample shutter;
+ private Storage storage = null!;
+
+ private Sample? shutter;
[BackgroundDependencyLoader]
- private void load(OsuConfigManager config, Storage storage, AudioManager audio)
+ private void load(Storage storage, AudioManager audio)
{
this.storage = storage.GetStorageForDirectory(@"screenshots");
-
- screenshotFormat = config.GetBindable(OsuSetting.ScreenshotFormat);
- captureMenuCursor = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor);
-
shutter = audio.Samples.Get("UI/shutter");
}
@@ -72,7 +68,7 @@ namespace osu.Game.Graphics
switch (e.Action)
{
case GlobalAction.TakeScreenshot:
- shutter.Play();
+ shutter?.Play();
TakeScreenshotAsync().FireAndForget();
return true;
}
@@ -90,9 +86,12 @@ namespace osu.Game.Graphics
{
Interlocked.Increment(ref screenShotTasks);
+ ScreenshotFormat screenshotFormat = config.Get(OsuSetting.ScreenshotFormat);
+ bool captureMenuCursor = config.Get(OsuSetting.ScreenshotCaptureMenuCursor);
+
try
{
- if (!captureMenuCursor.Value)
+ if (!captureMenuCursor)
{
cursorVisibility.Value = false;
@@ -101,7 +100,7 @@ namespace osu.Game.Graphics
int framesWaited = 0;
- using (var framesWaitedEvent = new ManualResetEventSlim(false))
+ using (ManualResetEventSlim framesWaitedEvent = new ManualResetEventSlim(false))
{
ScheduledDelegate waitDelegate = host.DrawThread.Scheduler.AddDelayed(() =>
{
@@ -117,17 +116,41 @@ namespace osu.Game.Graphics
}
}
- using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false))
+ using (Image? image = await host.TakeScreenshotAsync().ConfigureAwait(false))
{
+ if (config.Get(OsuSetting.Scaling) == ScalingMode.Everything)
+ {
+ float posX = config.Get(OsuSetting.ScalingPositionX);
+ float posY = config.Get(OsuSetting.ScalingPositionY);
+ float sizeX = config.Get(OsuSetting.ScalingSizeX);
+ float sizeY = config.Get(OsuSetting.ScalingSizeY);
+
+ image.Mutate(m =>
+ {
+ Rectangle rect = new Rectangle(Point.Empty, m.GetCurrentSize());
+
+ // Reduce size by user scale settings...
+ int sx = (rect.Width - (int)(rect.Width * sizeX)) / 2;
+ int sy = (rect.Height - (int)(rect.Height * sizeY)) / 2;
+ rect.Inflate(-sx, -sy);
+
+ // ...then adjust the region based on their positional offset.
+ rect.X = (int)(rect.X * posX) * 2;
+ rect.Y = (int)(rect.Y * posY) * 2;
+
+ m.Crop(rect);
+ });
+ }
+
clipboard.SetImage(image);
- (string filename, var stream) = getWritableStream();
+ (string? filename, Stream? stream) = getWritableStream(screenshotFormat);
if (filename == null) return;
using (stream)
{
- switch (screenshotFormat.Value)
+ switch (screenshotFormat)
{
case ScreenshotFormat.Png:
await image.SaveAsPngAsync(stream).ConfigureAwait(false);
@@ -140,7 +163,7 @@ namespace osu.Game.Graphics
break;
default:
- throw new InvalidOperationException($"Unknown enum member {nameof(ScreenshotFormat)} {screenshotFormat.Value}.");
+ throw new InvalidOperationException($"Unknown enum member {nameof(ScreenshotFormat)} {screenshotFormat}.");
}
}
@@ -164,12 +187,12 @@ namespace osu.Game.Graphics
private static readonly object filename_reservation_lock = new object();
- private (string filename, Stream stream) getWritableStream()
+ private (string? filename, Stream? stream) getWritableStream(ScreenshotFormat format)
{
lock (filename_reservation_lock)
{
- var dt = DateTime.Now;
- string fileExt = screenshotFormat.ToString().ToLowerInvariant();
+ DateTime dt = DateTime.Now;
+ string fileExt = format.ToString().ToLowerInvariant();
string withoutIndex = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}.{fileExt}";
if (!storage.Exists(withoutIndex))
diff --git a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs
index af4b3849af..4af6ce7498 100644
--- a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs
+++ b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Graphics.UserInterface
@@ -48,6 +49,7 @@ namespace osu.Game.Graphics.UserInterface
{
protected virtual float ChevronSize => 10;
+ [CanBeNull]
public event Action StateChanged;
public readonly SpriteIcon Chevron;
diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs
index 255b2149f0..62cdefda43 100644
--- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs
+++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs
@@ -4,7 +4,6 @@
#nullable disable
using osu.Framework.Bindables;
-using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
@@ -39,7 +38,7 @@ namespace osu.Game.Graphics.UserInterface
protected override double GetProportionalDuration(long currentValue, long newValue) =>
currentValue > newValue ? currentValue - newValue : newValue - currentValue;
- protected override LocalisableString FormatCount(long count) => count.ToLocalisableString(formatString);
+ protected override LocalisableString FormatCount(long count) => count.ToString(formatString);
protected override OsuSpriteText CreateSpriteText()
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true));
diff --git a/osu.Game/IO/IStorageResourceProvider.cs b/osu.Game/IO/IStorageResourceProvider.cs
index 08982a8b5f..91760971e8 100644
--- a/osu.Game/IO/IStorageResourceProvider.cs
+++ b/osu.Game/IO/IStorageResourceProvider.cs
@@ -41,6 +41,6 @@ namespace osu.Game.IO
///
/// The underlying provider of texture data (in arbitrary image formats).
/// A texture loader store.
- IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore);
+ IResourceStore? CreateTextureLoaderStore(IResourceStore underlyingStore);
}
}
diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs
index 14a3c5a43c..d03d259f71 100644
--- a/osu.Game/IO/MigratableStorage.cs
+++ b/osu.Game/IO/MigratableStorage.cs
@@ -6,8 +6,8 @@
using System;
using System.IO;
using System.Linq;
-using System.Threading;
using osu.Framework.Platform;
+using osu.Game.Utils;
namespace osu.Game.IO
{
@@ -81,7 +81,7 @@ namespace osu.Game.IO
if (IgnoreSuffixes.Any(suffix => fi.Name.EndsWith(suffix, StringComparison.Ordinal)))
continue;
- allFilesDeleted &= AttemptOperation(() => fi.Delete(), throwOnFailure: false);
+ allFilesDeleted &= FileUtils.AttemptOperation(() => fi.Delete(), throwOnFailure: false);
}
foreach (DirectoryInfo dir in target.GetDirectories())
@@ -92,11 +92,11 @@ namespace osu.Game.IO
if (IgnoreSuffixes.Any(suffix => dir.Name.EndsWith(suffix, StringComparison.Ordinal)))
continue;
- allFilesDeleted &= AttemptOperation(() => dir.Delete(true), throwOnFailure: false);
+ allFilesDeleted &= FileUtils.AttemptOperation(() => dir.Delete(true), throwOnFailure: false);
}
if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0)
- allFilesDeleted &= AttemptOperation(target.Delete, throwOnFailure: false);
+ allFilesDeleted &= FileUtils.AttemptOperation(target.Delete, throwOnFailure: false);
return allFilesDeleted;
}
@@ -115,7 +115,7 @@ namespace osu.Game.IO
if (IgnoreSuffixes.Any(suffix => fileInfo.Name.EndsWith(suffix, StringComparison.Ordinal)))
continue;
- AttemptOperation(() =>
+ FileUtils.AttemptOperation(() =>
{
fileInfo.Refresh();
@@ -139,35 +139,5 @@ namespace osu.Game.IO
CopyRecursive(dir, destination.CreateSubdirectory(dir.Name), false);
}
}
-
- ///
- /// Attempt an IO operation multiple times and only throw if none of the attempts succeed.
- ///
- /// The action to perform.
- /// The number of attempts (250ms wait between each).
- /// Whether to throw an exception on failure. If false, will silently fail.
- protected static bool AttemptOperation(Action action, int attempts = 10, bool throwOnFailure = true)
- {
- while (true)
- {
- try
- {
- action();
- return true;
- }
- catch (Exception)
- {
- if (attempts-- == 0)
- {
- if (throwOnFailure)
- throw;
-
- return false;
- }
- }
-
- Thread.Sleep(250);
- }
- }
}
}
diff --git a/osu.Game/Localisation/ModSelectOverlayStrings.cs b/osu.Game/Localisation/ModSelectOverlayStrings.cs
index 86ebebd293..9513eacf02 100644
--- a/osu.Game/Localisation/ModSelectOverlayStrings.cs
+++ b/osu.Game/Localisation/ModSelectOverlayStrings.cs
@@ -49,6 +49,26 @@ namespace osu.Game.Localisation
///
public static LocalisableString ScoreMultiplier => new TranslatableString(getKey(@"score_multiplier"), @"Score Multiplier");
+ ///
+ /// "Ranked"
+ ///
+ public static LocalisableString Ranked => new TranslatableString(getKey(@"ranked"), @"Ranked");
+
+ ///
+ /// "Performance points can be granted for the active mods."
+ ///
+ public static LocalisableString RankedExplanation => new TranslatableString(getKey(@"ranked_explanation"), @"Performance points can be granted for the active mods.");
+
+ ///
+ /// "Unranked"
+ ///
+ public static LocalisableString Unranked => new TranslatableString(getKey(@"unranked"), @"Unranked");
+
+ ///
+ /// "Performance points will not be granted due to active mods."
+ ///
+ public static LocalisableString UnrankedExplanation => new TranslatableString(getKey(@"ranked_explanation"), @"Performance points will not be granted due to active mods.");
+
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
diff --git a/osu.Game/Localisation/ToolbarStrings.cs b/osu.Game/Localisation/ToolbarStrings.cs
index e71a3fff9b..5822f76e02 100644
--- a/osu.Game/Localisation/ToolbarStrings.cs
+++ b/osu.Game/Localisation/ToolbarStrings.cs
@@ -19,6 +19,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString Connecting => new TranslatableString(getKey(@"connecting"), @"Connecting...");
+ ///
+ /// "Verification required"
+ ///
+ public static LocalisableString VerificationRequired => new TranslatableString(getKey(@"verification_required"), @"Verification required");
+
///
/// "home"
///
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 17bf8bcc37..d3707fe74d 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -21,7 +21,7 @@ using osu.Game.Configuration;
using osu.Game.Localisation;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Online.Notifications;
+using osu.Game.Online.Chat;
using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Users;
@@ -48,6 +48,8 @@ namespace osu.Game.Online.API
public string ProvidedUsername { get; private set; }
+ public string SecondFactorCode { get; private set; }
+
private string password;
public IBindable LocalUser => localUser;
@@ -55,6 +57,8 @@ namespace osu.Game.Online.API
public IBindable Activity => activity;
public IBindable Statistics => statistics;
+ public INotificationsClient NotificationsClient { get; }
+
public Language Language => game.CurrentLanguage.Value;
private Bindable localUser { get; } = new Bindable(createGuestUser());
@@ -82,6 +86,7 @@ namespace osu.Game.Online.API
APIEndpointUrl = endpointConfiguration.APIEndpointUrl;
WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl;
+ NotificationsClient = setUpNotificationsClient();
authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl);
log = Logger.GetLogger(LoggingTarget.Network);
@@ -114,6 +119,30 @@ namespace osu.Game.Online.API
thread.Start();
}
+ private WebSocketNotificationsClientConnector setUpNotificationsClient()
+ {
+ var connector = new WebSocketNotificationsClientConnector(this);
+
+ connector.MessageReceived += msg =>
+ {
+ switch (msg.Event)
+ {
+ case @"verified":
+ if (state.Value == APIState.RequiresSecondFactorAuth)
+ state.Value = APIState.Online;
+ break;
+
+ case @"logout":
+ if (state.Value == APIState.Online)
+ Logout();
+
+ break;
+ }
+ };
+
+ return connector;
+ }
+
private void onTokenChanged(ValueChangedEvent e) => config.SetValue(OsuSetting.Token, config.Get(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty);
internal new void Schedule(Action action) => base.Schedule(action);
@@ -197,6 +226,7 @@ namespace osu.Game.Online.API
///
///
/// This method takes control of and transitions from to either
+ /// - (pending 2fa)
/// - (successful connection)
/// - (failed connection but retrying)
/// - (failed and can't retry, clear credentials and require user interaction)
@@ -204,8 +234,6 @@ namespace osu.Game.Online.API
/// Whether the connection attempt was successful.
private void attemptConnect()
{
- state.Value = APIState.Connecting;
-
if (localUser.IsDefault)
{
// Show a placeholder user if saved credentials are available.
@@ -223,6 +251,7 @@ namespace osu.Game.Online.API
if (!authentication.HasValidAccessToken)
{
+ state.Value = APIState.Connecting;
LastLoginError = null;
try
@@ -240,40 +269,79 @@ namespace osu.Game.Online.API
}
}
- var userReq = new GetUserRequest();
- userReq.Failure += ex =>
+ switch (state.Value)
{
- if (ex is APIException)
+ case APIState.RequiresSecondFactorAuth:
{
- LastLoginError = ex;
- log.Add($@"Login failed for username {ProvidedUsername} on user retrieval ({LastLoginError.Message})!");
- Logout();
+ if (string.IsNullOrEmpty(SecondFactorCode))
+ return;
+
+ state.Value = APIState.Connecting;
+ LastLoginError = null;
+
+ var verificationRequest = new VerifySessionRequest(SecondFactorCode);
+
+ verificationRequest.Success += () => state.Value = APIState.Online;
+ verificationRequest.Failure += ex =>
+ {
+ state.Value = APIState.RequiresSecondFactorAuth;
+ LastLoginError = ex;
+ SecondFactorCode = null;
+ };
+
+ if (!handleRequest(verificationRequest))
+ {
+ state.Value = APIState.Failing;
+ return;
+ }
+
+ if (state.Value != APIState.Online)
+ return;
+
+ break;
}
- else if (ex is WebException webException && webException.Message == @"Unauthorized")
+
+ default:
{
- log.Add(@"Login no longer valid");
- Logout();
+ var userReq = new GetMeRequest();
+
+ userReq.Failure += ex =>
+ {
+ if (ex is APIException)
+ {
+ LastLoginError = ex;
+ log.Add($@"Login failed for username {ProvidedUsername} on user retrieval ({LastLoginError.Message})!");
+ Logout();
+ }
+ else if (ex is WebException webException && webException.Message == @"Unauthorized")
+ {
+ log.Add(@"Login no longer valid");
+ Logout();
+ }
+ else
+ {
+ state.Value = APIState.Failing;
+ }
+ };
+
+ userReq.Success += me =>
+ {
+ me.Status.Value = configStatus.Value ?? UserStatus.Online;
+
+ setLocalUser(me);
+
+ state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth;
+ failureCount = 0;
+ };
+
+ if (!handleRequest(userReq))
+ {
+ state.Value = APIState.Failing;
+ return;
+ }
+
+ break;
}
- else
- {
- state.Value = APIState.Failing;
- }
- };
- userReq.Success += user =>
- {
- user.Status.Value = configStatus.Value ?? UserStatus.Online;
-
- setLocalUser(user);
-
- // we're connected!
- state.Value = APIState.Online;
- failureCount = 0;
- };
-
- if (!handleRequest(userReq))
- {
- state.Value = APIState.Failing;
- return;
}
var friendsReq = new GetFriendsRequest();
@@ -321,11 +389,17 @@ namespace osu.Game.Online.API
this.password = password;
}
+ public void AuthenticateSecondFactor(string code)
+ {
+ Debug.Assert(State.Value == APIState.RequiresSecondFactorAuth);
+
+ SecondFactorCode = code;
+ }
+
public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) =>
new HubClientConnector(clientName, endpoint, this, versionHash, preferMessagePack);
- public NotificationsClientConnector GetNotificationsConnector() =>
- new WebSocketNotificationsClientConnector(this);
+ public IChatClient GetChatClient() => new WebSocketChatClient(this);
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
{
@@ -507,6 +581,7 @@ namespace osu.Game.Online.API
public void Logout()
{
password = null;
+ SecondFactorCode = null;
authentication.Clear();
// Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present
@@ -566,6 +641,11 @@ namespace osu.Game.Online.API
///
Failing,
+ ///
+ /// Waiting on second factor authentication.
+ ///
+ RequiresSecondFactorAuth,
+
///
/// We are in the process of (re-)connecting.
///
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index 4b4f8061e0..4962838bd9 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.cs
@@ -7,8 +7,10 @@ using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Localisation;
+using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Online.Notifications;
+using osu.Game.Online.Chat;
+using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Tests;
using osu.Game.Users;
@@ -30,6 +32,9 @@ namespace osu.Game.Online.API
public Bindable Statistics { get; } = new Bindable();
+ public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient();
+ INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient;
+
public Language Language => Language.en;
public string AccessToken => "token";
@@ -57,6 +62,7 @@ namespace osu.Game.Online.API
private bool shouldFailNextLogin;
private bool stayConnectingNextLogin;
+ private bool requiredSecondFactorAuth = true;
///
/// The current connectivity state of the API.
@@ -117,13 +123,46 @@ namespace osu.Game.Online.API
Id = DUMMY_USER_ID,
};
+ if (requiredSecondFactorAuth)
+ {
+ state.Value = APIState.RequiresSecondFactorAuth;
+ }
+ else
+ {
+ onSuccessfulLogin();
+ requiredSecondFactorAuth = true;
+ }
+ }
+
+ public void AuthenticateSecondFactor(string code)
+ {
+ var request = new VerifySessionRequest(code);
+ request.Failure += e =>
+ {
+ state.Value = APIState.RequiresSecondFactorAuth;
+ LastLoginError = e;
+ };
+
+ state.Value = APIState.Connecting;
+ LastLoginError = null;
+
+ // if no handler installed / handler can't handle verification, just assume that the server would verify for simplicity.
+ if (HandleRequest?.Invoke(request) != true)
+ onSuccessfulLogin();
+
+ // if a handler did handle this, make sure the verification actually passed.
+ if (request.CompletionState == APIRequestCompletionState.Completed)
+ onSuccessfulLogin();
+ }
+
+ private void onSuccessfulLogin()
+ {
+ state.Value = APIState.Online;
Statistics.Value = new UserStatistics
{
GlobalRank = 1,
CountryRank = 1
};
-
- state.Value = APIState.Online;
}
public void Logout()
@@ -144,7 +183,7 @@ namespace osu.Game.Online.API
public IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null;
- public NotificationsClientConnector GetNotificationsConnector() => new PollingNotificationsClientConnector(this);
+ public IChatClient GetChatClient() => new TestChatClientConnector(this);
public RegistrationRequest.RegistrationRequestErrors? CreateAccount(string email, string username, string password)
{
@@ -159,6 +198,11 @@ namespace osu.Game.Online.API
IBindable IAPIProvider.Activity => Activity;
IBindable IAPIProvider.Statistics => Statistics;
+ ///
+ /// Skip 2FA requirement for next login.
+ ///
+ public void SkipSecondFactor() => requiredSecondFactorAuth = false;
+
///
/// During the next simulated login, the process will fail immediately.
///
diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs
index b58d4a363a..66f124f7c3 100644
--- a/osu.Game/Online/API/IAPIProvider.cs
+++ b/osu.Game/Online/API/IAPIProvider.cs
@@ -6,7 +6,8 @@ using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Game.Localisation;
using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Online.Notifications;
+using osu.Game.Online.Chat;
+using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Users;
namespace osu.Game.Online.API
@@ -111,6 +112,12 @@ namespace osu.Game.Online.API
/// The user's password.
void Login(string username, string password);
+ ///
+ /// Provide a second-factor authentication code for authentication.
+ ///
+ /// The 2FA code.
+ void AuthenticateSecondFactor(string code);
+
///
/// Log out the current user.
///
@@ -130,9 +137,14 @@ namespace osu.Game.Online.API
IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack = true);
///
- /// Constructs a new .
+ /// Accesses the used to receive asynchronous notifications from web.
///
- NotificationsClientConnector GetNotificationsConnector();
+ INotificationsClient NotificationsClient { get; }
+
+ ///
+ /// Creates a instance to use in order to chat.
+ ///
+ IChatClient GetChatClient();
///
/// Create a new user account. This is a blocking operation.
diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs
index 485274f349..4829310870 100644
--- a/osu.Game/Online/API/OAuth.cs
+++ b/osu.Game/Online/API/OAuth.cs
@@ -128,19 +128,12 @@ namespace osu.Game.Online.API
// if we already have a valid access token, let's use it.
if (accessTokenValid) return true;
- // we want to ensure only a single authentication update is happening at once.
- lock (access_token_retrieval_lock)
- {
- // re-check if valid, in case another request completed and revalidated our access.
- if (accessTokenValid) return true;
+ // if not, let's try using our refresh token to request a new access token.
+ if (!string.IsNullOrEmpty(Token.Value?.RefreshToken))
+ // ReSharper disable once PossibleNullReferenceException
+ AuthenticateWithRefresh(Token.Value.RefreshToken);
- // if not, let's try using our refresh token to request a new access token.
- if (!string.IsNullOrEmpty(Token.Value?.RefreshToken))
- // ReSharper disable once PossibleNullReferenceException
- AuthenticateWithRefresh(Token.Value.RefreshToken);
-
- return accessTokenValid;
- }
+ return accessTokenValid;
}
private bool accessTokenValid => Token.Value?.IsValid ?? false;
@@ -149,14 +142,18 @@ namespace osu.Game.Online.API
internal string RequestAccessToken()
{
- if (!ensureAccessToken()) return null;
+ lock (access_token_retrieval_lock)
+ {
+ if (!ensureAccessToken()) return null;
- return Token.Value.AccessToken;
+ return Token.Value.AccessToken;
+ }
}
internal void Clear()
{
- Token.Value = null;
+ lock (access_token_retrieval_lock)
+ Token.Value = null;
}
private class AccessTokenRequestRefresh : AccessTokenRequest
diff --git a/osu.Game/Online/API/Requests/GetMeRequest.cs b/osu.Game/Online/API/Requests/GetMeRequest.cs
new file mode 100644
index 0000000000..aab7d7b2f1
--- /dev/null
+++ b/osu.Game/Online/API/Requests/GetMeRequest.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Rulesets;
+
+namespace osu.Game.Online.API.Requests
+{
+ public class GetMeRequest : APIRequest
+ {
+ public readonly IRulesetInfo? Ruleset;
+
+ ///
+ /// Gets the currently logged-in user.
+ ///
+ /// The ruleset to get the user's info for.
+ public GetMeRequest(IRulesetInfo? ruleset = null)
+ {
+ Ruleset = ruleset;
+ }
+
+ protected override string Target => $@"me/{Ruleset?.ShortName}";
+ }
+}
diff --git a/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs b/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs
index 659e46bb11..52ca0c11eb 100644
--- a/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs
+++ b/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
@@ -9,7 +8,7 @@ namespace osu.Game.Online.API.Requests
public class GetSystemTitleRequest : OsuJsonWebRequest
{
public GetSystemTitleRequest()
- : base($@"https://assets.ppy.sh/lazer-status.json?{DateTimeOffset.UtcNow.ToUnixTimeSeconds() / 1800}")
+ : base(@"https://assets.ppy.sh/lazer-status.json")
{
}
}
diff --git a/osu.Game/Online/API/Requests/GetUserRequest.cs b/osu.Game/Online/API/Requests/GetUserRequest.cs
index 7dcf75950e..90d3268e75 100644
--- a/osu.Game/Online/API/Requests/GetUserRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUserRequest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
@@ -11,24 +9,17 @@ namespace osu.Game.Online.API.Requests
public class GetUserRequest : APIRequest
{
public readonly string Lookup;
- public readonly IRulesetInfo Ruleset;
+ public readonly IRulesetInfo? Ruleset;
private readonly LookupType lookupType;
- ///
- /// Gets the currently logged-in user.
- ///
- public GetUserRequest()
- {
- }
-
///
/// Gets a user from their ID.
///
/// The user to get.
/// The ruleset to get the user's info for.
- public GetUserRequest(long? userId = null, IRulesetInfo ruleset = null)
+ public GetUserRequest(long? userId = null, IRulesetInfo? ruleset = null)
{
- Lookup = userId.ToString();
+ Lookup = userId.ToString()!;
lookupType = LookupType.Id;
Ruleset = ruleset;
}
@@ -38,14 +29,14 @@ namespace osu.Game.Online.API.Requests
///
/// The user to get.
/// The ruleset to get the user's info for.
- public GetUserRequest(string username = null, IRulesetInfo ruleset = null)
+ public GetUserRequest(string username, IRulesetInfo? ruleset = null)
{
Lookup = username;
lookupType = LookupType.Username;
Ruleset = ruleset;
}
- protected override string Target => Lookup != null ? $@"users/{Lookup}/{Ruleset?.ShortName}?key={lookupType.ToString().ToLowerInvariant()}" : $@"me/{Ruleset?.ShortName}";
+ protected override string Target => $@"users/{Lookup}/{Ruleset?.ShortName}?key={lookupType.ToString().ToLowerInvariant()}";
private enum LookupType
{
diff --git a/osu.Game/Online/API/Requests/ReissueVerificationCodeRequest.cs b/osu.Game/Online/API/Requests/ReissueVerificationCodeRequest.cs
new file mode 100644
index 0000000000..f2a3cf0a16
--- /dev/null
+++ b/osu.Game/Online/API/Requests/ReissueVerificationCodeRequest.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Net.Http;
+using osu.Framework.IO.Network;
+
+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";
+ }
+}
diff --git a/osu.Game/Online/API/Requests/Responses/APIMe.cs b/osu.Game/Online/API/Requests/Responses/APIMe.cs
new file mode 100644
index 0000000000..3cbddbe5e7
--- /dev/null
+++ b/osu.Game/Online/API/Requests/Responses/APIMe.cs
@@ -0,0 +1,13 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Newtonsoft.Json;
+
+namespace osu.Game.Online.API.Requests.Responses
+{
+ public class APIMe : APIUser
+ {
+ [JsonProperty("session_verified")]
+ public bool SessionVerified { get; set; }
+ }
+}
diff --git a/osu.Game/Online/API/Requests/VerifySessionRequest.cs b/osu.Game/Online/API/Requests/VerifySessionRequest.cs
new file mode 100644
index 0000000000..b39ec5b79a
--- /dev/null
+++ b/osu.Game/Online/API/Requests/VerifySessionRequest.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Net.Http;
+using osu.Framework.IO.Network;
+
+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";
+ }
+}
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index 23989caae2..74e85c595c 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -16,7 +16,6 @@ using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Online.Notifications;
using osu.Game.Overlays.Chat.Listing;
namespace osu.Game.Online.Chat
@@ -64,13 +63,8 @@ namespace osu.Game.Online.Chat
///
public IBindableList AvailableChannels => availableChannels;
- ///
- /// Whether the client responsible for channel notifications is connected.
- ///
- public bool NotificationsConnected => connector.IsConnected.Value;
-
private readonly IAPIProvider api;
- private readonly NotificationsClientConnector connector;
+ private readonly IChatClient chatClient;
[Resolved]
private UserLookupCache users { get; set; }
@@ -85,7 +79,7 @@ namespace osu.Game.Online.Chat
{
this.api = api;
- connector = api.GetNotificationsConnector();
+ chatClient = api.GetChatClient();
CurrentChannel.ValueChanged += currentChannelChanged;
}
@@ -93,15 +87,11 @@ namespace osu.Game.Online.Chat
[BackgroundDependencyLoader]
private void load()
{
- connector.ChannelJoined += ch => Schedule(() => joinChannel(ch));
-
- connector.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false));
-
- connector.NewMessages += msgs => Schedule(() => addMessages(msgs));
-
- connector.PresenceReceived += () => Schedule(initializeChannels);
-
- connector.Start();
+ chatClient.ChannelJoined += ch => Schedule(() => joinChannel(ch));
+ chatClient.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false));
+ chatClient.NewMessages += msgs => Schedule(() => addMessages(msgs));
+ chatClient.PresenceReceived += () => Schedule(initializeChannels);
+ chatClient.RequestPresence();
apiState.BindTo(api.State);
apiState.BindValueChanged(_ => SendAck(), true);
@@ -655,7 +645,7 @@ namespace osu.Game.Online.Chat
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
- connector?.Dispose();
+ chatClient?.Dispose();
}
}
diff --git a/osu.Game/Online/Chat/IChatClient.cs b/osu.Game/Online/Chat/IChatClient.cs
new file mode 100644
index 0000000000..290ee22710
--- /dev/null
+++ b/osu.Game/Online/Chat/IChatClient.cs
@@ -0,0 +1,39 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+
+namespace osu.Game.Online.Chat
+{
+ ///
+ /// Interface for consuming online chat.
+ ///
+ public interface IChatClient : IDisposable
+ {
+ ///
+ /// Fired when a has been joined.
+ ///
+ event Action? ChannelJoined;
+
+ ///
+ /// Fired when a has been parted.
+ ///
+ event Action? ChannelParted;
+
+ ///
+ /// Fired when new s have arrived from the server.
+ ///
+ event Action>? NewMessages;
+
+ ///
+ /// Requests presence information from the server.
+ ///
+ void RequestPresence();
+
+ ///
+ /// Fired when the initial user presence information has been received.
+ ///
+ event Action? PresenceReceived;
+ }
+}
diff --git a/osu.Game/Online/Chat/WebSocketChatClient.cs b/osu.Game/Online/Chat/WebSocketChatClient.cs
new file mode 100644
index 0000000000..b74f8bec4b
--- /dev/null
+++ b/osu.Game/Online/Chat/WebSocketChatClient.cs
@@ -0,0 +1,173 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Concurrent;
+using System.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? ChannelJoined;
+ public event Action? ChannelParted;
+ public event Action>? NewMessages;
+ public event Action? PresenceReceived;
+
+ private readonly IAPIProvider api;
+ private readonly INotificationsClient client;
+ private readonly ConcurrentDictionary channelsMap = new ConcurrentDictionary();
+
+ private CancellationTokenSource? chatStartCancellationSource;
+
+ public WebSocketChatClient(IAPIProvider api)
+ {
+ this.api = api;
+ client = api.NotificationsClient;
+ client.IsConnected.BindValueChanged(onConnectedChanged, true);
+ }
+
+ private void onConnectedChanged(ValueChangedEvent 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(message.Data.ToString());
+ Debug.Assert(joinedChannel != null);
+
+ joinChannel(joinedChannel);
+ break;
+
+ case @"chat.channel.part":
+ Debug.Assert(message.Data != null);
+
+ Channel? partedChannel = JsonConvert.DeserializeObject(message.Data.ToString());
+ Debug.Assert(partedChannel != null);
+
+ partChannel(partedChannel);
+ break;
+
+ case @"chat.message.new":
+ Debug.Assert(message.Data != null);
+
+ NewChatMessageData? messageData = JsonConvert.DeserializeObject(message.Data.ToString());
+ Debug.Assert(messageData != null);
+
+ foreach (var msg in messageData.Messages)
+ postToChannel(msg);
+
+ break;
+ }
+ }
+
+ private void postToChannel(Message message)
+ {
+ if (channelsMap.TryGetValue(message.ChannelId, out Channel? channel))
+ {
+ joinChannel(channel);
+ NewMessages?.Invoke(new List { message });
+ return;
+ }
+
+ var req = new GetChannelRequest(message.ChannelId);
+
+ req.Success += response =>
+ {
+ joinChannel(channelsMap[message.ChannelId] = response.Channel);
+ NewMessages?.Invoke(new List { 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? messages)
+ {
+ if (messages == null)
+ return;
+
+ NewMessages?.Invoke(messages);
+ }
+
+ public void Dispose()
+ {
+ client.IsConnected.ValueChanged -= onConnectedChanged;
+ client.MessageReceived -= onMessageReceived;
+ }
+ }
+}
diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs
index f3bcced630..bd3c945124 100644
--- a/osu.Game/Online/EndpointConfiguration.cs
+++ b/osu.Game/Online/EndpointConfiguration.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Online
{
///
@@ -13,36 +11,36 @@ namespace osu.Game.Online
///
/// The base URL for the website.
///
- public string WebsiteRootUrl { get; set; }
+ public string WebsiteRootUrl { get; set; } = string.Empty;
///
/// The endpoint for the main (osu-web) API.
///
- public string APIEndpointUrl { get; set; }
+ public string APIEndpointUrl { get; set; } = string.Empty;
///
/// The OAuth client secret.
///
- public string APIClientSecret { get; set; }
+ public string APIClientSecret { get; set; } = string.Empty;
///
/// The OAuth client ID.
///
- public string APIClientID { get; set; }
+ public string APIClientID { get; set; } = string.Empty;
///
/// The endpoint for the SignalR spectator server.
///
- public string SpectatorEndpointUrl { get; set; }
+ public string SpectatorEndpointUrl { get; set; } = string.Empty;
///
/// The endpoint for the SignalR multiplayer server.
///
- public string MultiplayerEndpointUrl { get; set; }
+ public string MultiplayerEndpointUrl { get; set; } = string.Empty;
///
/// The endpoint for the SignalR metadata server.
///
- public string MetadataEndpointUrl { get; set; }
+ public string MetadataEndpointUrl { get; set; } = string.Empty;
}
}
diff --git a/osu.Game/Online/ExperimentalEndpointConfiguration.cs b/osu.Game/Online/ExperimentalEndpointConfiguration.cs
deleted file mode 100644
index c3d0014c8b..0000000000
--- a/osu.Game/Online/ExperimentalEndpointConfiguration.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) ppy Pty Ltd . 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";
- }
- }
-}
diff --git a/osu.Game/Online/Notifications/NotificationsClientConnector.cs b/osu.Game/Online/Notifications/NotificationsClientConnector.cs
deleted file mode 100644
index 34ce186cb8..0000000000
--- a/osu.Game/Online/Notifications/NotificationsClientConnector.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using osu.Game.Online.API;
-using osu.Game.Online.Chat;
-
-namespace osu.Game.Online.Notifications
-{
- ///
- /// An abstract connector or s.
- ///
- public abstract class NotificationsClientConnector : PersistentEndpointClientConnector
- {
- public event Action? ChannelJoined;
- public event Action? ChannelParted;
- public event Action>? NewMessages;
- public event Action? PresenceReceived;
-
- protected NotificationsClientConnector(IAPIProvider api)
- : base(api)
- {
- }
-
- protected sealed override async Task BuildConnectionAsync(CancellationToken cancellationToken)
- {
- var client = await BuildNotificationClientAsync(cancellationToken).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 BuildNotificationClientAsync(CancellationToken cancellationToken);
- }
-}
diff --git a/osu.Game/Online/Notifications/WebSocket/DummyNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/DummyNotificationsClient.cs
new file mode 100644
index 0000000000..c1f3d25be7
--- /dev/null
+++ b/osu.Game/Online/Notifications/WebSocket/DummyNotificationsClient.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using osu.Framework.Bindables;
+
+namespace osu.Game.Online.Notifications.WebSocket
+{
+ public class DummyNotificationsClient : INotificationsClient
+ {
+ public IBindable IsConnected => new BindableBool(true);
+
+ public event Action? MessageReceived;
+
+ public Func? 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);
+ }
+}
diff --git a/osu.Game/Online/Notifications/WebSocket/INotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/INotificationsClient.cs
new file mode 100644
index 0000000000..9a222d0fdd
--- /dev/null
+++ b/osu.Game/Online/Notifications/WebSocket/INotificationsClient.cs
@@ -0,0 +1,31 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using osu.Framework.Bindables;
+
+namespace osu.Game.Online.Notifications.WebSocket
+{
+ ///
+ /// A client for asynchronous notifications sent by osu-web.
+ ///
+ public interface INotificationsClient
+ {
+ ///
+ /// Whether this is currently connected to a server.
+ ///
+ IBindable IsConnected { get; }
+
+ ///
+ /// Invoked when a new arrives for this client.
+ ///
+ event Action? MessageReceived;
+
+ ///
+ /// Sends a to the notification server.
+ ///
+ Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = default);
+ }
+}
diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs
index 73e5dcec6f..854f46880f 100644
--- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs
+++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net;
using System.Net.WebSockets;
@@ -12,23 +11,20 @@ using System.Threading.Tasks;
using Newtonsoft.Json;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Logging;
-using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
-using osu.Game.Online.Chat;
namespace osu.Game.Online.Notifications.WebSocket
{
///
/// A notifications client which receives events via a websocket.
///
- public class WebSocketNotificationsClient : NotificationsClient
+ public class WebSocketNotificationsClient : PersistentEndpointClient
{
+ public event Action? MessageReceived;
+
private readonly ClientWebSocket socket;
private readonly string endpoint;
- private readonly ConcurrentDictionary channelsMap = new ConcurrentDictionary();
- public WebSocketNotificationsClient(ClientWebSocket socket, string endpoint, IAPIProvider api)
- : base(api)
+ public WebSocketNotificationsClient(ClientWebSocket socket, string endpoint)
{
this.socket = socket;
this.endpoint = endpoint;
@@ -37,11 +33,7 @@ namespace osu.Game.Online.Notifications.WebSocket
public override async Task ConnectAsync(CancellationToken cancellationToken)
{
await socket.ConnectAsync(new Uri(endpoint), cancellationToken).ConfigureAwait(false);
- await sendMessage(new StartChatRequest(), CancellationToken.None).ConfigureAwait(false);
-
runReadLoop(cancellationToken);
-
- await base.ConnectAsync(cancellationToken).ConfigureAwait(false);
}
private void runReadLoop(CancellationToken cancellationToken) => Task.Run(async () =>
@@ -73,7 +65,7 @@ namespace osu.Game.Online.Notifications.WebSocket
break;
}
- await onMessageReceivedAsync(message).ConfigureAwait(false);
+ MessageReceived?.Invoke(message);
}
break;
@@ -105,69 +97,12 @@ namespace osu.Game.Online.Notifications.WebSocket
}
}
- private async Task sendMessage(SocketMessage message, CancellationToken cancellationToken)
+ public async Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = default)
{
if (socket.State != WebSocketState.Open)
return;
- await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
- }
-
- private async Task onMessageReceivedAsync(SocketMessage message)
- {
- switch (message.Event)
- {
- case @"chat.channel.join":
- Debug.Assert(message.Data != null);
-
- Channel? joinedChannel = JsonConvert.DeserializeObject(message.Data.ToString());
- Debug.Assert(joinedChannel != null);
-
- HandleChannelJoined(joinedChannel);
- break;
-
- case @"chat.channel.part":
- Debug.Assert(message.Data != null);
-
- Channel? partedChannel = JsonConvert.DeserializeObject(message.Data.ToString());
- Debug.Assert(partedChannel != null);
-
- HandleChannelParted(partedChannel);
- break;
-
- case @"chat.message.new":
- Debug.Assert(message.Data != null);
-
- NewChatMessageData? messageData = JsonConvert.DeserializeObject(message.Data.ToString());
- Debug.Assert(messageData != null);
-
- foreach (var msg in messageData.Messages)
- HandleChannelJoined(await getChannel(msg.ChannelId).ConfigureAwait(false));
-
- HandleMessages(messageData.Messages);
- break;
- }
- }
-
- private async Task getChannel(long channelId)
- {
- if (channelsMap.TryGetValue(channelId, out Channel? channel))
- return channel;
-
- var tsc = new TaskCompletionSource();
- var req = new GetChannelRequest(channelId);
-
- req.Success += response =>
- {
- channelsMap[channelId] = response.Channel;
- tsc.SetResult(response.Channel);
- };
-
- req.Failure += ex => tsc.SetException(ex);
-
- API.Queue(req);
-
- return await tsc.Task.ConfigureAwait(false);
+ await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken ?? CancellationToken.None).ConfigureAwait(false);
}
public override async ValueTask DisposeAsync()
diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs
index f50369a06c..596322d377 100644
--- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs
+++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Net;
using System.Net.WebSockets;
using System.Threading;
@@ -13,26 +14,26 @@ namespace osu.Game.Online.Notifications.WebSocket
///
/// A connector for s that receive events via a websocket.
///
- public class WebSocketNotificationsClientConnector : NotificationsClientConnector
+ public class WebSocketNotificationsClientConnector : PersistentEndpointClientConnector, INotificationsClient
{
+ public event Action? MessageReceived;
+
private readonly IAPIProvider api;
public WebSocketNotificationsClientConnector(IAPIProvider api)
: base(api)
{
this.api = api;
+ Start();
}
- protected override async Task BuildNotificationClientAsync(CancellationToken cancellationToken)
+ protected override async Task BuildConnectionAsync(CancellationToken cancellationToken)
{
- var tcs = new TaskCompletionSource();
-
var req = new GetNotificationsRequest();
- req.Success += bundle => tcs.SetResult(bundle.Endpoint);
- req.Failure += ex => tcs.SetException(ex);
- api.Queue(req);
-
- string endpoint = await tcs.Task.ConfigureAwait(false);
+ // must use `PerformAsync()`, since we may not be fully online yet
+ // (see `APIState.RequiresSecondFactorAuth` - in this state queued requests will not execute).
+ await api.PerformAsync(req).ConfigureAwait(false);
+ string endpoint = req.Response!.Endpoint;
ClientWebSocket socket = new ClientWebSocket();
socket.Options.SetRequestHeader(@"Authorization", @$"Bearer {api.AccessToken}");
@@ -40,7 +41,17 @@ namespace osu.Game.Online.Notifications.WebSocket
if (socket.Options.Proxy != null)
socket.Options.Proxy.Credentials = CredentialCache.DefaultCredentials;
- return new WebSocketNotificationsClient(socket, endpoint, api);
+ var client = new WebSocketNotificationsClient(socket, endpoint);
+ client.MessageReceived += msg => MessageReceived?.Invoke(msg);
+ return client;
+ }
+
+ public Task SendAsync(SocketMessage message, CancellationToken? cancellationToken = default)
+ {
+ if (CurrentConnection is not WebSocketNotificationsClient webSocketClient)
+ return Task.CompletedTask;
+
+ return webSocketClient.SendAsync(message, cancellationToken);
}
}
}
diff --git a/osu.Game/Online/OnlineStatusNotifier.cs b/osu.Game/Online/OnlineStatusNotifier.cs
index c36e4ab894..dda430ce6f 100644
--- a/osu.Game/Online/OnlineStatusNotifier.cs
+++ b/osu.Game/Online/OnlineStatusNotifier.cs
@@ -11,6 +11,7 @@ using osu.Framework.Screens;
using osu.Game.Online.API;
using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
@@ -25,6 +26,8 @@ namespace osu.Game.Online
{
private readonly Func getCurrentScreen;
+ private INotificationsClient notificationsClient = null!;
+
[Resolved]
private MultiplayerClient multiplayerClient { get; set; } = null!;
@@ -55,9 +58,11 @@ namespace osu.Game.Online
private void load(IAPIProvider api)
{
apiState = api.State.GetBoundCopy();
+ notificationsClient = api.NotificationsClient;
multiplayerState = multiplayerClient.IsConnected.GetBoundCopy();
spectatorState = spectatorClient.IsConnected.GetBoundCopy();
+ notificationsClient.MessageReceived += notifyAboutForcedDisconnection;
multiplayerClient.Disconnecting += notifyAboutForcedDisconnection;
spectatorClient.Disconnecting += notifyAboutForcedDisconnection;
metadataClient.Disconnecting += notifyAboutForcedDisconnection;
@@ -127,10 +132,27 @@ namespace osu.Game.Online
});
}
+ private void notifyAboutForcedDisconnection(SocketMessage obj)
+ {
+ if (obj.Event != @"logout") return;
+
+ if (userNotified) return;
+
+ userNotified = true;
+ notificationOverlay?.Post(new SimpleErrorNotification
+ {
+ Icon = FontAwesome.Solid.ExclamationCircle,
+ Text = "You have been logged out due to a change to your account. Please log in again."
+ });
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
+ if (notificationsClient.IsNotNull())
+ notificationsClient.MessageReceived += notifyAboutForcedDisconnection;
+
if (spectatorClient.IsNotNull())
spectatorClient.Disconnecting -= notifyAboutForcedDisconnection;
diff --git a/osu.Game/Online/OnlineViewContainer.cs b/osu.Game/Online/OnlineViewContainer.cs
index 46f64fbb61..824da152b2 100644
--- a/osu.Game/Online/OnlineViewContainer.cs
+++ b/osu.Game/Online/OnlineViewContainer.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -79,10 +80,14 @@ namespace osu.Game.Online
case APIState.Failing:
case APIState.Connecting:
+ case APIState.RequiresSecondFactorAuth:
PopContentOut(Content);
LoadingSpinner.Show();
placeholder.FadeOut(transform_duration / 2, Easing.OutQuint);
break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
}
});
diff --git a/osu.Game/Online/PersistentEndpointClientConnector.cs b/osu.Game/Online/PersistentEndpointClientConnector.cs
index 8c1b58a750..024a0fea73 100644
--- a/osu.Game/Online/PersistentEndpointClientConnector.cs
+++ b/osu.Game/Online/PersistentEndpointClientConnector.cs
@@ -69,6 +69,7 @@ namespace osu.Game.Online
break;
case APIState.Online:
+ case APIState.RequiresSecondFactorAuth:
await connect().ConfigureAwait(true);
break;
}
@@ -83,7 +84,7 @@ namespace osu.Game.Online
try
{
- while (apiState.Value == APIState.Online)
+ while (apiState.Value == APIState.RequiresSecondFactorAuth || apiState.Value == APIState.Online)
{
// ensure any previous connection was disposed.
// this will also create a new cancellation token source.
diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs
index 7911701853..07ee9115d6 100644
--- a/osu.Game/Online/Spectator/SpectatorClient.cs
+++ b/osu.Game/Online/Spectator/SpectatorClient.cs
@@ -264,13 +264,12 @@ namespace osu.Game.Online.Spectator
{
Debug.Assert(ThreadSafety.IsUpdateThread);
- if (watchedUsersRefCounts.ContainsKey(userId))
+ if (!watchedUsersRefCounts.TryAdd(userId, 1))
{
watchedUsersRefCounts[userId]++;
return;
}
- watchedUsersRefCounts.Add(userId, 1);
WatchUserInternal(userId);
}
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 4e465f59df..a2a6322665 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -102,7 +102,7 @@ namespace osu.Game
public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild;
public virtual EndpointConfiguration CreateEndpoints() =>
- UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ExperimentalEndpointConfiguration();
+ UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version();
@@ -340,10 +340,6 @@ namespace osu.Game
dependencies.Cache(beatmapCache = new BeatmapLookupCache());
base.Content.Add(beatmapCache);
- var scorePerformanceManager = new ScorePerformanceCache();
- dependencies.Cache(scorePerformanceManager);
- base.Content.Add(scorePerformanceManager);
-
dependencies.CacheAs(rulesetConfigCache = new RulesetConfigCache(realm, RulesetStore));
var powerStatus = CreateBatteryInfo();
diff --git a/osu.Game/Overlays/AccountCreation/ScreenEmailVerification.cs b/osu.Game/Overlays/AccountCreation/ScreenEmailVerification.cs
new file mode 100644
index 0000000000..f3b42117ea
--- /dev/null
+++ b/osu.Game/Overlays/AccountCreation/ScreenEmailVerification.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using 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,
+ };
+ }
+ }
+}
diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
index 9ad507d82a..f57c7d22a2 100644
--- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
+++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
@@ -1,12 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -28,28 +27,30 @@ namespace osu.Game.Overlays.AccountCreation
{
public partial class ScreenEntry : AccountCreationScreen
{
- private ErrorTextFlowContainer usernameDescription;
- private ErrorTextFlowContainer emailAddressDescription;
- private ErrorTextFlowContainer passwordDescription;
+ private ErrorTextFlowContainer usernameDescription = null!;
+ private ErrorTextFlowContainer emailAddressDescription = null!;
+ private ErrorTextFlowContainer passwordDescription = null!;
- private OsuTextBox usernameTextBox;
- private OsuTextBox emailTextBox;
- private OsuPasswordTextBox passwordTextBox;
+ private OsuTextBox usernameTextBox = null!;
+ private OsuTextBox emailTextBox = null!;
+ private OsuPasswordTextBox passwordTextBox = null!;
[Resolved]
- private IAPIProvider api { get; set; }
+ private IAPIProvider api { get; set; } = null!;
- private ShakeContainer registerShake;
- private ITextPart characterCheckText;
+ private IBindable apiState = null!;
- private OsuTextBox[] textboxes;
- private LoadingLayer loadingLayer;
+ private ShakeContainer registerShake = null!;
+ private ITextPart characterCheckText = null!;
+
+ private OsuTextBox[] textboxes = null!;
+ private LoadingLayer loadingLayer = null!;
[Resolved]
- private GameHost host { get; set; }
+ private GameHost? host { get; set; }
[Resolved]
- private OsuGame game { get; set; }
+ private OsuGame? game { get; set; }
[BackgroundDependencyLoader]
private void load()
@@ -144,6 +145,8 @@ namespace osu.Game.Overlays.AccountCreation
passwordTextBox.Current.BindValueChanged(_ => updateCharacterCheckTextColour(), true);
characterCheckText.DrawablePartsRecreated += _ => updateCharacterCheckTextColour();
+
+ apiState = api.State.GetBoundCopy();
}
private void updateCharacterCheckTextColour()
@@ -180,7 +183,7 @@ namespace osu.Game.Overlays.AccountCreation
Task.Run(() =>
{
bool success;
- RegistrationRequest.RegistrationRequestErrors errors = null;
+ RegistrationRequest.RegistrationRequestErrors? errors = null;
try
{
@@ -210,7 +213,7 @@ namespace osu.Game.Overlays.AccountCreation
if (!string.IsNullOrEmpty(errors.Message))
passwordDescription.AddErrors(new[] { errors.Message });
- game.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", true);
+ game?.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", true);
}
}
else
@@ -223,6 +226,12 @@ namespace osu.Game.Overlays.AccountCreation
return;
}
+ apiState.BindValueChanged(state =>
+ {
+ if (state.NewValue == APIState.RequiresSecondFactorAuth)
+ this.Push(new ScreenEmailVerification());
+ });
+
api.Login(usernameTextBox.Text, passwordTextBox.Text);
});
});
@@ -241,6 +250,6 @@ namespace osu.Game.Overlays.AccountCreation
return false;
}
- private OsuTextBox nextUnfilledTextBox() => textboxes.FirstOrDefault(t => string.IsNullOrEmpty(t.Text));
+ private OsuTextBox? nextUnfilledTextBox() => textboxes.FirstOrDefault(t => string.IsNullOrEmpty(t.Text));
}
}
diff --git a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs
index 0fbf6ba59e..c24bd32bb4 100644
--- a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs
+++ b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -23,14 +21,14 @@ namespace osu.Game.Overlays.AccountCreation
{
public partial class ScreenWarning : AccountCreationScreen
{
- private OsuTextFlowContainer multiAccountExplanationText;
- private LinkFlowContainer furtherAssistance;
+ private OsuTextFlowContainer multiAccountExplanationText = null!;
+ private LinkFlowContainer furtherAssistance = null!;
- [Resolved(canBeNull: true)]
- private IAPIProvider api { get; set; }
+ [Resolved]
+ private IAPIProvider? api { get; set; }
- [Resolved(canBeNull: true)]
- private OsuGame game { get; set; }
+ [Resolved]
+ private OsuGame? game { get; set; }
private const string help_centre_url = "/help/wiki/Help_Centre#login";
diff --git a/osu.Game/Overlays/AccountCreationOverlay.cs b/osu.Game/Overlays/AccountCreationOverlay.cs
index ef2e055eae..82fc5508f1 100644
--- a/osu.Game/Overlays/AccountCreationOverlay.cs
+++ b/osu.Game/Overlays/AccountCreationOverlay.cs
@@ -1,8 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@@ -24,7 +23,9 @@ namespace osu.Game.Overlays
{
private const float transition_time = 400;
- private ScreenWelcome welcomeScreen;
+ private ScreenWelcome welcomeScreen = null!;
+
+ private ScheduledDelegate? scheduledHide;
public AccountCreationOverlay()
{
@@ -107,8 +108,6 @@ namespace osu.Game.Overlays
this.FadeOut(100);
}
- private ScheduledDelegate scheduledHide;
-
private void apiStateChanged(ValueChangedEvent state)
{
switch (state.NewValue)
@@ -118,12 +117,16 @@ namespace osu.Game.Overlays
break;
case APIState.Connecting:
+ case APIState.RequiresSecondFactorAuth:
break;
case APIState.Online:
scheduledHide?.Cancel();
scheduledHide = Schedule(Hide);
break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
}
}
}
diff --git a/osu.Game/Overlays/Comments/VotePill.cs b/osu.Game/Overlays/Comments/VotePill.cs
index dd418a9e58..8c5aaa062f 100644
--- a/osu.Game/Overlays/Comments/VotePill.cs
+++ b/osu.Game/Overlays/Comments/VotePill.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Comments
public Color4 AccentColour { get; set; }
- protected override IEnumerable EffectTargets => null;
+ protected override IEnumerable EffectTargets => Enumerable.Empty();
[Resolved]
private IAPIProvider api { get; set; }
diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs
index 0eef55162f..80dfca93d2 100644
--- a/osu.Game/Overlays/Login/LoginForm.cs
+++ b/osu.Game/Overlays/Login/LoginForm.cs
@@ -32,13 +32,7 @@ namespace osu.Game.Overlays.Login
public Action? RequestHide;
- private void performLogin()
- {
- if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text))
- api.Login(username.Text, password.Text);
- else
- shakeSignIn.Shake();
- }
+ public override bool AcceptsFocus => true;
[BackgroundDependencyLoader(permitNulls: true)]
private void load(OsuConfigManager config, AccountCreationOverlay accountCreation)
@@ -144,7 +138,13 @@ namespace osu.Game.Overlays.Login
}
}
- public override bool AcceptsFocus => true;
+ private void performLogin()
+ {
+ if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text))
+ api.Login(username.Text, password.Text);
+ else
+ shakeSignIn.Shake();
+ }
protected override bool OnClick(ClickEvent e) => true;
diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs
index ea65656eb1..25bf612bc3 100644
--- a/osu.Game/Overlays/Login/LoginPanel.cs
+++ b/osu.Game/Overlays/Login/LoginPanel.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Login
{
private bool bounding = true;
- private LoginForm? form;
+ private Drawable? form;
[Resolved]
private OsuColour colours { get; set; } = null!;
@@ -81,6 +81,10 @@ namespace osu.Game.Overlays.Login
};
break;
+ case APIState.RequiresSecondFactorAuth:
+ Child = form = new SecondFactorAuthForm();
+ break;
+
case APIState.Failing:
case APIState.Connecting:
LinkFlowContainer linkFlow;
diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs
new file mode 100644
index 0000000000..dcd3119f33
--- /dev/null
+++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs
@@ -0,0 +1,147 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Threading.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); });
+ }
+ }
+}
diff --git a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs
index a25147b69f..f4f6fd2bc1 100644
--- a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs
+++ b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs
@@ -4,6 +4,7 @@
#nullable disable
using System;
+using JetBrains.Annotations;
using osu.Framework;
using osuTK;
using osu.Framework.Allocation;
@@ -24,6 +25,7 @@ namespace osu.Game.Overlays.MedalSplash
private const float scale_when_unlocked = 0.76f;
private const float scale_when_full = 0.6f;
+ [CanBeNull]
public event Action StateChanged;
private readonly Medal medal;
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index 7271c53e7a..ddf96c1cb3 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -125,7 +125,7 @@ namespace osu.Game.Overlays.Mods
private DeselectAllModsButton deselectAllModsButton = null!;
private Container aboveColumnsContent = null!;
- private ScoreMultiplierDisplay? multiplierDisplay;
+ private RankingInformationDisplay? rankingInformationDisplay;
private BeatmapAttributesDisplay? beatmapAttributesDisplay;
protected ShearedButton BackButton { get; private set; } = null!;
@@ -185,7 +185,7 @@ namespace osu.Game.Overlays.Mods
aboveColumnsContent = new Container
{
RelativeSizeAxes = Axes.X,
- Height = ScoreMultiplierDisplay.HEIGHT,
+ Height = RankingInformationDisplay.HEIGHT,
Padding = new MarginPadding { Horizontal = 100 },
Child = SearchTextBox = new ShearedSearchTextBox
{
@@ -200,7 +200,7 @@ namespace osu.Game.Overlays.Mods
{
Padding = new MarginPadding
{
- Top = ScoreMultiplierDisplay.HEIGHT + PADDING,
+ Top = RankingInformationDisplay.HEIGHT + PADDING,
Bottom = PADDING
},
RelativeSizeAxes = Axes.Both,
@@ -269,7 +269,7 @@ namespace osu.Game.Overlays.Mods
},
Children = new Drawable[]
{
- multiplierDisplay = new ScoreMultiplierDisplay
+ rankingInformationDisplay = new RankingInformationDisplay
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight
@@ -315,7 +315,7 @@ namespace osu.Game.Overlays.Mods
SelectedMods.BindValueChanged(_ =>
{
- updateMultiplier();
+ updateRankingInformation();
updateFromExternalSelection();
updateCustomisation();
@@ -328,7 +328,7 @@ namespace osu.Game.Overlays.Mods
//
// See https://github.com/ppy/osu/pull/23284#issuecomment-1529056988
modSettingChangeTracker = new ModSettingChangeTracker(SelectedMods.Value);
- modSettingChangeTracker.SettingChanged += _ => updateMultiplier();
+ modSettingChangeTracker.SettingChanged += _ => updateRankingInformation();
}
}, true);
@@ -450,9 +450,9 @@ namespace osu.Game.Overlays.Mods
modState.ValidForSelection.Value = modState.Mod.Type != ModType.System && modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod);
}
- private void updateMultiplier()
+ private void updateRankingInformation()
{
- if (multiplierDisplay == null)
+ if (rankingInformationDisplay == null)
return;
double multiplier = 1.0;
@@ -460,7 +460,8 @@ namespace osu.Game.Overlays.Mods
foreach (var mod in SelectedMods.Value)
multiplier *= mod.ScoreMultiplier;
- multiplierDisplay.Current.Value = multiplier;
+ rankingInformationDisplay.ModMultiplier.Value = multiplier;
+ rankingInformationDisplay.Ranked.Value = SelectedMods.Value.All(m => m.Ranked);
}
private void updateCustomisation()
diff --git a/osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs b/osu.Game/Overlays/Mods/RankingInformationDisplay.cs
similarity index 60%
rename from osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs
rename to osu.Game/Overlays/Mods/RankingInformationDisplay.cs
index a86eba81e4..494f8a377f 100644
--- a/osu.Game/Overlays/Mods/ScoreMultiplierDisplay.cs
+++ b/osu.Game/Overlays/Mods/RankingInformationDisplay.cs
@@ -6,8 +6,8 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@@ -22,15 +22,13 @@ namespace osu.Game.Overlays.Mods
///
/// On the mod select overlay, this provides a local updating view of the aggregate score multiplier coming from mods.
///
- public partial class ScoreMultiplierDisplay : ModFooterInformationDisplay, IHasCurrentValue
+ public partial class RankingInformationDisplay : ModFooterInformationDisplay
{
public const float HEIGHT = 42;
- public Bindable Current
- {
- get => current.Current;
- set => current.Current = value;
- }
+ public Bindable ModMultiplier = new BindableDouble(1);
+
+ public Bindable Ranked { get; } = new BindableBool(true);
private readonly BindableWithCurrent current = new BindableWithCurrent();
@@ -39,16 +37,11 @@ namespace osu.Game.Overlays.Mods
private RollingCounter counter = null!;
private Box flashLayer = null!;
+ private TextWithTooltip rankedText = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
- public ScoreMultiplierDisplay()
- {
- Current.Default = 1d;
- Current.Value = 1d;
- }
-
[BackgroundDependencyLoader]
private void load()
{
@@ -75,13 +68,20 @@ namespace osu.Game.Overlays.Mods
LeftContent.AddRange(new Drawable[]
{
- new OsuSpriteText
+ new Container
{
+ Width = 50,
+ RelativeSizeAxes = Axes.Y,
+ Margin = new MarginPadding(10),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
- Text = ModSelectOverlayStrings.ScoreMultiplier,
- Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
+ Child = rankedText = new TextWithTooltip
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
+ Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
+ }
}
});
@@ -97,7 +97,7 @@ namespace osu.Game.Overlays.Mods
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Current = { BindTarget = Current }
+ Current = { BindTarget = ModMultiplier }
}
});
}
@@ -106,30 +106,22 @@ namespace osu.Game.Overlays.Mods
{
base.LoadComplete();
- Current.BindValueChanged(e =>
+ ModMultiplier.BindValueChanged(e =>
{
- if (e.NewValue > Current.Default)
+ if (e.NewValue > ModMultiplier.Default)
{
- MainBackground
- .FadeColour(colours.ForModType(ModType.DifficultyIncrease), transition_duration, Easing.OutQuint);
- counter.FadeColour(ColourProvider.Background5, transition_duration, Easing.OutQuint);
+ counter.FadeColour(colours.ForModType(ModType.DifficultyIncrease), transition_duration, Easing.OutQuint);
}
- else if (e.NewValue < Current.Default)
+ else if (e.NewValue < ModMultiplier.Default)
{
- MainBackground
- .FadeColour(colours.ForModType(ModType.DifficultyReduction), transition_duration, Easing.OutQuint);
- counter.FadeColour(ColourProvider.Background5, transition_duration, Easing.OutQuint);
+ counter.FadeColour(colours.ForModType(ModType.DifficultyReduction), transition_duration, Easing.OutQuint);
}
else
{
- MainBackground.FadeColour(ColourProvider.Background4, transition_duration, Easing.OutQuint);
counter.FadeColour(Colour4.White, transition_duration, Easing.OutQuint);
}
- flashLayer
- .FadeOutFromOne()
- .FadeTo(0.15f, 60, Easing.OutQuint)
- .Then().FadeOut(500, Easing.OutQuint);
+ flash();
const float move_amount = 4;
if (e.NewValue > e.OldValue)
@@ -140,10 +132,43 @@ namespace osu.Game.Overlays.Mods
// required to prevent the counter initially rolling up from 0 to 1
// due to `Current.Value` having a nonstandard default value of 1.
- counter.SetCountWithoutRolling(Current.Value);
+ counter.SetCountWithoutRolling(ModMultiplier.Value);
+
+ Ranked.BindValueChanged(e =>
+ {
+ flash();
+
+ if (e.NewValue)
+ {
+ rankedText.Text = ModSelectOverlayStrings.Ranked;
+ rankedText.TooltipText = ModSelectOverlayStrings.RankedExplanation;
+ rankedText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint);
+ FrontBackground.FadeColour(ColourProvider.Background3, transition_duration, Easing.OutQuint);
+ }
+ else
+ {
+ rankedText.Text = ModSelectOverlayStrings.Unranked;
+ rankedText.TooltipText = ModSelectOverlayStrings.UnrankedExplanation;
+ rankedText.FadeColour(ColourProvider.Background5, transition_duration, Easing.OutQuint);
+ FrontBackground.FadeColour(colours.Orange1, transition_duration, Easing.OutQuint);
+ }
+ }, true);
}
- private partial class EffectCounter : RollingCounter
+ private void flash()
+ {
+ flashLayer
+ .FadeOutFromOne()
+ .FadeTo(0.15f, 60, Easing.OutQuint)
+ .Then().FadeOut(500, Easing.OutQuint);
+ }
+
+ private partial class TextWithTooltip : OsuSpriteText, IHasTooltip
+ {
+ public LocalisableString TooltipText { get; set; }
+ }
+
+ private partial class EffectCounter : RollingCounter, IHasTooltip
{
protected override double RollingDuration => 250;
@@ -155,6 +180,8 @@ namespace osu.Game.Overlays.Mods
Origin = Anchor.Centre,
Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
};
+
+ public LocalisableString TooltipText => ModSelectOverlayStrings.ScoreMultiplier;
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
index 3ff5556f4d..fe88413e6a 100644
--- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
@@ -15,6 +15,7 @@ using osu.Game.Localisation;
using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.Settings.Sections.Maintenance;
using osu.Game.Updater;
+using osu.Game.Utils;
using SharpCompress.Archives.Zip;
namespace osu.Game.Overlays.Settings.Sections.General
@@ -111,7 +112,8 @@ namespace osu.Game.Overlays.Settings.Sections.General
using (var outStream = storage.CreateFileSafely(archive_filename))
using (var zip = ZipArchive.Create())
{
- foreach (string? f in logStorage.GetFiles(string.Empty, "*.log")) zip.AddEntry(f, logStorage.GetStream(f), true);
+ foreach (string? f in logStorage.GetFiles(string.Empty, "*.log"))
+ FileUtils.AttemptOperation(z => z.AddEntry(f, logStorage.GetStream(f), true), zip);
zip.SaveTo(outStream);
}
diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs
index 230974cd59..2620e850c8 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs
@@ -95,7 +95,7 @@ namespace osu.Game.Overlays.Toolbar
private void onlineStateChanged(ValueChangedEvent state) => Schedule(() =>
{
- failingIcon.FadeTo(state.NewValue == APIState.Failing ? 1 : 0, 200, Easing.OutQuint);
+ failingIcon.FadeTo(state.NewValue == APIState.Failing || state.NewValue == APIState.RequiresSecondFactorAuth ? 1 : 0, 200, Easing.OutQuint);
switch (state.NewValue)
{
@@ -107,6 +107,13 @@ namespace osu.Game.Overlays.Toolbar
case APIState.Failing:
TooltipText = ToolbarStrings.AttemptingToReconnect;
spinner.Show();
+ failingIcon.Icon = FontAwesome.Solid.ExclamationTriangle;
+ break;
+
+ case APIState.RequiresSecondFactorAuth:
+ TooltipText = ToolbarStrings.VerificationRequired;
+ spinner.Show();
+ failingIcon.Icon = FontAwesome.Solid.Key;
break;
case APIState.Offline:
diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs
index 9ca4c25ab9..6ec4971f06 100644
--- a/osu.Game/Overlays/Volume/VolumeMeter.cs
+++ b/osu.Game/Overlays/Volume/VolumeMeter.cs
@@ -5,6 +5,7 @@
using System;
using System.Globalization;
+using JetBrains.Annotations;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -48,6 +49,7 @@ namespace osu.Game.Overlays.Volume
private Sample notchSample;
private double sampleLastPlaybackTime;
+ [CanBeNull]
public event Action StateChanged;
private SelectionState state;
diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs
index ad9257d4f3..4563c264f7 100644
--- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs
@@ -21,21 +21,29 @@ namespace osu.Game.Rulesets.Difficulty
{
private readonly IBeatmap playableBeatmap;
private readonly BeatmapDifficultyCache difficultyCache;
- private readonly ScorePerformanceCache performanceCache;
- public PerformanceBreakdownCalculator(IBeatmap playableBeatmap, BeatmapDifficultyCache difficultyCache, ScorePerformanceCache performanceCache)
+ public PerformanceBreakdownCalculator(IBeatmap playableBeatmap, BeatmapDifficultyCache difficultyCache)
{
this.playableBeatmap = playableBeatmap;
this.difficultyCache = difficultyCache;
- this.performanceCache = performanceCache;
}
[ItemCanBeNull]
public async Task CalculateAsync(ScoreInfo score, CancellationToken cancellationToken = default)
{
+ var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
+
+ var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator();
+
+ // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value.
+ if (attributes?.Attributes == null || performanceCalculator == null)
+ return null;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
PerformanceAttributes[] performanceArray = await Task.WhenAll(
// compute actual performance
- performanceCache.CalculatePerformanceAsync(score, cancellationToken),
+ performanceCalculator.CalculateAsync(score, attributes.Value.Attributes, cancellationToken),
// compute performance for perfect play
getPerfectPerformance(score, cancellationToken)
).ConfigureAwait(false);
@@ -88,8 +96,12 @@ namespace osu.Game.Rulesets.Difficulty
cancellationToken
).ConfigureAwait(false);
- // ScorePerformanceCache is not used to avoid caching multiple copies of essentially identical perfect performance attributes
- return difficulty == null ? null : ruleset.CreatePerformanceCalculator()?.Calculate(perfectPlay, difficulty.Value.Attributes.AsNonNull());
+ var performanceCalculator = ruleset.CreatePerformanceCalculator();
+
+ if (performanceCalculator == null || difficulty == null)
+ return null;
+
+ return await performanceCalculator.CalculateAsync(perfectPlay, difficulty.Value.Attributes.AsNonNull(), cancellationToken).ConfigureAwait(false);
}, cancellationToken);
}
diff --git a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs
index f5e826f8c7..966da0ff12 100644
--- a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Threading;
+using System.Threading.Tasks;
using osu.Game.Beatmaps;
using osu.Game.Scoring;
@@ -15,6 +17,9 @@ namespace osu.Game.Rulesets.Difficulty
Ruleset = ruleset;
}
+ public Task CalculateAsync(ScoreInfo score, DifficultyAttributes attributes, CancellationToken cancellationToken)
+ => Task.Run(() => CreatePerformanceAttributes(score, attributes), cancellationToken);
+
public PerformanceAttributes Calculate(ScoreInfo score, DifficultyAttributes attributes)
=> CreatePerformanceAttributes(score, attributes);
diff --git a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs
index 7330f138ce..61b72a6066 100644
--- a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs
+++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs
@@ -10,7 +10,7 @@ using osuTK;
namespace osu.Game.Rulesets.Judgements
{
- public partial class DefaultJudgementPiece : JudgementPiece, IAnimatableJudgement
+ public partial class DefaultJudgementPiece : TextJudgementPiece, IAnimatableJudgement
{
public DefaultJudgementPiece(HitResult result)
: base(result)
@@ -38,20 +38,6 @@ namespace osu.Game.Rulesets.Judgements
///
public virtual void PlayAnimation()
{
- // TODO: make these better. currently they are using a text `-` and it's not centered properly.
- // Should be an explicit drawable.
- //
- // When this is done, remove the [Description] attributes from HitResults which were added for this purpose.
- if (Result == HitResult.IgnoreMiss || Result == HitResult.LargeTickMiss)
- {
- this.RotateTo(-45);
- this.ScaleTo(1.6f);
- this.ScaleTo(1.2f, 100, Easing.In);
-
- this.FadeOutFromOne(400);
- return;
- }
-
if (Result.IsMiss())
{
this.ScaleTo(1.6f);
diff --git a/osu.Game/Rulesets/Judgements/JudgementPiece.cs b/osu.Game/Rulesets/Judgements/TextJudgementPiece.cs
similarity index 88%
rename from osu.Game/Rulesets/Judgements/JudgementPiece.cs
rename to osu.Game/Rulesets/Judgements/TextJudgementPiece.cs
index 03f211c318..42527705eb 100644
--- a/osu.Game/Rulesets/Judgements/JudgementPiece.cs
+++ b/osu.Game/Rulesets/Judgements/TextJudgementPiece.cs
@@ -10,7 +10,7 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Judgements
{
- public abstract partial class JudgementPiece : CompositeDrawable
+ public abstract partial class TextJudgementPiece : CompositeDrawable
{
protected readonly HitResult Result;
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Judgements
[Resolved]
private OsuColour colours { get; set; } = null!;
- protected JudgementPiece(HitResult result)
+ protected TextJudgementPiece(HitResult result)
{
Result = result;
}
diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs
index 744d02a4fa..3a33d14835 100644
--- a/osu.Game/Rulesets/Mods/IMod.cs
+++ b/osu.Game/Rulesets/Mods/IMod.cs
@@ -66,6 +66,11 @@ namespace osu.Game.Rulesets.Mods
///
bool AlwaysValidForSubmission { get; }
+ ///
+ /// Whether scores with this mod active can give performance points.
+ ///
+ bool Ranked { get; }
+
///
/// Create a fresh instance based on this mod.
///
diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs
index 0500b49513..50c867f41b 100644
--- a/osu.Game/Rulesets/Mods/Mod.cs
+++ b/osu.Game/Rulesets/Mods/Mod.cs
@@ -167,6 +167,12 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore]
public virtual bool RequiresConfiguration => false;
+ ///
+ /// Whether scores with this mod active can give performance points.
+ ///
+ [JsonIgnore]
+ public virtual bool Ranked => false;
+
///
/// The mods this mod cannot be enabled with.
///
diff --git a/osu.Game/Rulesets/Mods/ModDaycore.cs b/osu.Game/Rulesets/Mods/ModDaycore.cs
index 09b35c249e..359f8a950c 100644
--- a/osu.Game/Rulesets/Mods/ModDaycore.cs
+++ b/osu.Game/Rulesets/Mods/ModDaycore.cs
@@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => null;
public override ModType Type => ModType.DifficultyReduction;
public override LocalisableString Description => "Whoaaaaa...";
+ public override bool Ranked => UsesDefaultConfiguration;
[SettingSource("Speed decrease", "The actual decrease to apply", SettingControlType = typeof(MultiplierSettingsSlider))]
public override BindableNumber SpeedChange { get; } = new BindableDouble(0.75)
diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs
index 789291772d..8e430da368 100644
--- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs
+++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs
@@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => OsuIcon.ModDoubleTime;
public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => "Zoooooooooom...";
+ public override bool Ranked => UsesDefaultConfiguration;
[SettingSource("Speed increase", "The actual increase to apply", SettingControlType = typeof(MultiplierSettingsSlider))]
public override BindableNumber SpeedChange { get; } = new BindableDouble(1.5)
diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs
index 0f51e2a6d5..da43a6b294 100644
--- a/osu.Game/Rulesets/Mods/ModEasy.cs
+++ b/osu.Game/Rulesets/Mods/ModEasy.cs
@@ -16,6 +16,7 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.DifficultyReduction;
public override double ScoreMultiplier => 0.5;
public override Type[] IncompatibleMods => new[] { typeof(ModHardRock), typeof(ModDifficultyAdjust) };
+ public override bool Ranked => UsesDefaultConfiguration;
public virtual void ReadFromDifficulty(BeatmapDifficulty difficulty)
{
diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs
index dc2ad6f47e..9227af64b8 100644
--- a/osu.Game/Rulesets/Mods/ModFlashlight.cs
+++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs
@@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => OsuIcon.ModFlashlight;
public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => "Restricted view area.";
+ public override bool Ranked => UsesDefaultConfiguration;
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public abstract BindableFloat SizeMultiplier { get; }
diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs
index 8b5dd39584..59e40ee9cc 100644
--- a/osu.Game/Rulesets/Mods/ModHalfTime.cs
+++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs
@@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => OsuIcon.ModHalftime;
public override ModType Type => ModType.DifficultyReduction;
public override LocalisableString Description => "Less zoom...";
+ public override bool Ranked => UsesDefaultConfiguration;
[SettingSource("Speed decrease", "The actual decrease to apply", SettingControlType = typeof(MultiplierSettingsSlider))]
public override BindableNumber SpeedChange { get; } = new BindableDouble(0.75)
diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs
index 4b2d1d050e..1e99891b99 100644
--- a/osu.Game/Rulesets/Mods/ModHardRock.cs
+++ b/osu.Game/Rulesets/Mods/ModHardRock.cs
@@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => "Everything just got a bit harder...";
public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModDifficultyAdjust) };
+ public override bool Ranked => UsesDefaultConfiguration;
protected const float ADJUST_RATIO = 1.4f;
diff --git a/osu.Game/Rulesets/Mods/ModHidden.cs b/osu.Game/Rulesets/Mods/ModHidden.cs
index 8b25768575..5a1abf115f 100644
--- a/osu.Game/Rulesets/Mods/ModHidden.cs
+++ b/osu.Game/Rulesets/Mods/ModHidden.cs
@@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Mods
public override string Acronym => "HD";
public override IconUsage? Icon => OsuIcon.ModHidden;
public override ModType Type => ModType.DifficultyIncrease;
+ public override bool Ranked => UsesDefaultConfiguration;
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
{
diff --git a/osu.Game/Rulesets/Mods/ModMuted.cs b/osu.Game/Rulesets/Mods/ModMuted.cs
index 131f501630..3ecd9aa6a1 100644
--- a/osu.Game/Rulesets/Mods/ModMuted.cs
+++ b/osu.Game/Rulesets/Mods/ModMuted.cs
@@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "Can you still feel the rhythm without music?";
public override ModType Type => ModType.Fun;
public override double ScoreMultiplier => 1;
+ public override bool Ranked => UsesDefaultConfiguration;
}
public abstract class ModMuted : ModMuted, IApplicableToDrawableRuleset, IApplicableToTrack, IApplicableToScoreProcessor
diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs
index b42927256c..bb18940f8c 100644
--- a/osu.Game/Rulesets/Mods/ModNightcore.cs
+++ b/osu.Game/Rulesets/Mods/ModNightcore.cs
@@ -28,6 +28,7 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => OsuIcon.ModNightcore;
public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => "Uguuuuuuuu...";
+ public override bool Ranked => UsesDefaultConfiguration;
[SettingSource("Speed increase", "The actual increase to apply", SettingControlType = typeof(MultiplierSettingsSlider))]
public override BindableNumber SpeedChange { get; } = new BindableDouble(1.5)
diff --git a/osu.Game/Rulesets/Mods/ModNoFail.cs b/osu.Game/Rulesets/Mods/ModNoFail.cs
index cc451772b2..1aaef8eac4 100644
--- a/osu.Game/Rulesets/Mods/ModNoFail.cs
+++ b/osu.Game/Rulesets/Mods/ModNoFail.cs
@@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "You can't fail, no matter what.";
public override double ScoreMultiplier => 0.5;
public override Type[] IncompatibleMods => new[] { typeof(ModFailCondition), typeof(ModCinema) };
+ public override bool Ranked => UsesDefaultConfiguration;
private readonly Bindable showHealthBar = new Bindable();
diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs
index 0ba40ba070..f8f498ceb5 100644
--- a/osu.Game/Rulesets/Mods/ModPerfect.cs
+++ b/osu.Game/Rulesets/Mods/ModPerfect.cs
@@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.DifficultyIncrease;
public override double ScoreMultiplier => 1;
public override LocalisableString Description => "SS or quit.";
+ public override bool Ranked => UsesDefaultConfiguration;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModSuddenDeath), typeof(ModAccuracyChallenge) }).ToArray();
diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
index 4e4e8662e8..62579a168c 100644
--- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
+++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
@@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => "Miss and fail.";
public override double ScoreMultiplier => 1;
+ public override bool Ranked => UsesDefaultConfiguration;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModPerfect)).ToArray();
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index c5f1878d1f..c9192ae3eb 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -11,6 +11,7 @@ using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
@@ -139,7 +140,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
protected override bool RequiresChildrenUpdate => true;
- public override bool IsPresent => base.IsPresent || (State.Value == ArmedState.Idle && Clock?.CurrentTime >= LifetimeStart);
+ public override bool IsPresent => base.IsPresent || (State.Value == ArmedState.Idle && Clock.IsNotNull() && Clock.CurrentTime >= LifetimeStart);
private readonly Bindable state = new Bindable();
@@ -782,6 +783,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (CurrentSkin != null)
CurrentSkin.SourceChanged -= skinSourceChanged;
+
+ // Safeties against shooting in foot in cases where these are bound by external entities (like playfield) that don't clean up.
+ OnNestedDrawableCreated = null;
+ OnNewResult = null;
+ OnRevertResult = null;
+ DefaultsApplied = null;
+ HitObjectApplied = null;
}
public Bindable AnimationStartTime { get; } = new BindableDouble();
diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs
index ec2a4a31f6..ef8bd08bf4 100644
--- a/osu.Game/Rulesets/Objects/HitObject.cs
+++ b/osu.Game/Rulesets/Objects/HitObject.cs
@@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.Objects
///
/// Invoked after has completed on this .
///
+ // TODO: This has no implicit unbind flow. Currently, if a Playfield manages HitObjects it will leave a bound event on this and cause the
+ // playfield to remain in memory.
public event Action DefaultsApplied;
public readonly Bindable StartTimeBindable = new BindableDouble();
diff --git a/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs b/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs
index fabf4fc444..7977166cb2 100644
--- a/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs
+++ b/osu.Game/Rulesets/Objects/Pooling/HitObjectEntryManager.cs
@@ -47,12 +47,9 @@ namespace osu.Game.Rulesets.Objects.Pooling
{
HitObject hitObject = entry.HitObject;
- if (entryMap.ContainsKey(hitObject))
+ if (!entryMap.TryAdd(hitObject, entry))
throw new InvalidOperationException($@"The {nameof(HitObjectLifetimeEntry)} is already added to this {nameof(HitObjectEntryManager)}.");
- // Add the entry.
- entryMap[hitObject] = entry;
-
// If the entry has a parent, set it and add the entry to the parent's children.
if (parent != null)
{
diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs
index 7e58df3cfa..20ec3c4946 100644
--- a/osu.Game/Rulesets/Scoring/HitResult.cs
+++ b/osu.Game/Rulesets/Scoring/HitResult.cs
@@ -86,7 +86,6 @@ namespace osu.Game.Rulesets.Scoring
/// Indicates a large tick miss.
///
[EnumMember(Value = "large_tick_miss")]
- [Description("-")]
[Order(11)]
LargeTickMiss,
@@ -118,7 +117,6 @@ namespace osu.Game.Rulesets.Scoring
/// Indicates a miss that should be ignored for scoring purposes.
///
[EnumMember(Value = "ignore_miss")]
- [Description("-")]
[Order(14)]
IgnoreMiss,
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index a092829317..9d12daad04 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -181,6 +181,8 @@ namespace osu.Game.Rulesets.Scoring
private readonly List hitEvents = new List();
private HitObject? lastHitObject;
+ public bool ApplyNewJudgementsWhenFailed { get; set; }
+
public ScoreProcessor(Ruleset ruleset)
{
Ruleset = ruleset;
@@ -211,7 +213,7 @@ namespace osu.Game.Rulesets.Scoring
result.ComboAtJudgement = Combo.Value;
result.HighestComboAtJudgement = HighestCombo.Value;
- if (result.FailedAtJudgement)
+ if (result.FailedAtJudgement && !ApplyNewJudgementsWhenFailed)
return;
ScoreResultCounts[result.Type] = ScoreResultCounts.GetValueOrDefault(result.Type) + 1;
@@ -267,7 +269,7 @@ namespace osu.Game.Rulesets.Scoring
Combo.Value = result.ComboAtJudgement;
HighestCombo.Value = result.HighestComboAtJudgement;
- if (result.FailedAtJudgement)
+ if (result.FailedAtJudgement && !ApplyNewJudgementsWhenFailed)
return;
ScoreResultCounts[result.Type] = ScoreResultCounts.GetValueOrDefault(result.Type) - 1;
diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs
index e9c35555c8..90a2f63faa 100644
--- a/osu.Game/Rulesets/UI/Playfield.cs
+++ b/osu.Game/Rulesets/UI/Playfield.cs
@@ -247,10 +247,14 @@ namespace osu.Game.Rulesets.UI
nestedPlayfields.Add(otherPlayfield);
}
+ private Mod[] mods;
+
protected override void LoadComplete()
{
base.LoadComplete();
+ mods = Mods?.ToArray();
+
// in the case a consumer forgets to add the HitObjectContainer, we will add it here.
if (HitObjectContainer.Parent == null)
AddInternal(HitObjectContainer);
@@ -260,9 +264,9 @@ namespace osu.Game.Rulesets.UI
{
base.Update();
- if (!IsNested && Mods != null)
+ if (!IsNested && mods != null)
{
- foreach (var mod in Mods)
+ foreach (Mod mod in mods)
{
if (mod is IUpdatableByPlayfield updatable)
updatable.Update(this);
@@ -403,10 +407,13 @@ namespace osu.Game.Rulesets.UI
// If this is the first time this DHO is being used, then apply the DHO mods.
// This is done before Apply() so that the state is updated once when the hitobject is applied.
- if (Mods != null)
+ if (mods != null)
{
- foreach (var m in Mods.OfType())
- m.ApplyToDrawableHitObject(dho);
+ foreach (Mod mod in mods)
+ {
+ if (mod is IApplicableToDrawableHitObject applicable)
+ applicable.ApplyToDrawableHitObject(dho);
+ }
}
}
diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs
index 041c7a13ae..a08c3bab08 100644
--- a/osu.Game/Rulesets/UI/RulesetInputManager.cs
+++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs
@@ -218,7 +218,7 @@ namespace osu.Game.Rulesets.UI
{
base.ReloadMappings(realmKeyBindings);
- KeyBindings = KeyBindings.Where(b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
+ KeyBindings = KeyBindings.Where(static b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
RealmKeyBindingStore.ClearDuplicateBindings(KeyBindings);
}
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index 129918da14..39ddb5c753 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
@@ -184,9 +184,12 @@ namespace osu.Game.Rulesets.UI.Scrolling
// We need to calculate hit object positions (including nested hit objects) as soon as possible after lifetimes
// to prevent hit objects displayed in a wrong position for one frame.
- // Only AliveObjects need to be considered for layout (reduces overhead in the case of scroll speed changes).
- foreach (var obj in AliveObjects)
+ // Only AliveEntries need to be considered for layout (reduces overhead in the case of scroll speed changes).
+ // We are not using AliveObjects directly to avoid selection/sorting overhead since we don't care about the order at which positions will be updated.
+ foreach (var entry in AliveEntries)
{
+ var obj = entry.Drawable;
+
updatePosition(obj, Time.Current);
if (layoutComputed.Contains(obj))
diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs
index e51a95798b..65e2c02655 100644
--- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs
+++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs
@@ -180,7 +180,7 @@ namespace osu.Game.Scoring.Legacy
///
/// The score to populate the statistics of.
/// The corresponding .
- internal static void PopulateMaximumStatistics(ScoreInfo score, WorkingBeatmap workingBeatmap)
+ public static void PopulateMaximumStatistics(ScoreInfo score, WorkingBeatmap workingBeatmap)
{
Debug.Assert(score.BeatmapInfo != null);
diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs
deleted file mode 100644
index 1f2b1aeb95..0000000000
--- a/osu.Game/Scoring/ScorePerformanceCache.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using osu.Framework.Allocation;
-using osu.Game.Beatmaps;
-using osu.Game.Database;
-using osu.Game.Rulesets.Difficulty;
-
-namespace osu.Game.Scoring
-{
- ///
- /// A component which performs and acts as a central cache for performance calculations of locally databased scores.
- /// Currently not persisted between game sessions.
- ///
- public partial class ScorePerformanceCache : MemoryCachingComponent
- {
- [Resolved]
- private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
-
- protected override bool CacheNullValues => false;
-
- ///
- /// Calculates performance for the given .
- ///
- /// The score to do the calculation on.
- /// An optional to cancel the operation.
- public Task CalculatePerformanceAsync(ScoreInfo score, CancellationToken token = default) =>
- GetAsync(new PerformanceCacheLookup(score), token);
-
- protected override async Task ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default)
- {
- var score = lookup.ScoreInfo;
-
- var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, token).ConfigureAwait(false);
-
- // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value.
- if (attributes?.Attributes == null)
- return null;
-
- token.ThrowIfCancellationRequested();
-
- return score.Ruleset.CreateInstance().CreatePerformanceCalculator()?.Calculate(score, attributes.Value.Attributes);
- }
-
- public readonly struct PerformanceCacheLookup
- {
- public readonly ScoreInfo ScoreInfo;
-
- public PerformanceCacheLookup(ScoreInfo info)
- {
- ScoreInfo = info;
- }
-
- public override int GetHashCode()
- {
- var hash = new HashCode();
-
- hash.Add(ScoreInfo.Hash);
- hash.Add(ScoreInfo.ID);
-
- return hash.ToHashCode();
- }
- }
- }
-}
diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
index e46b92795a..a552b22c11 100644
--- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
+++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Screens.Backgrounds
private Background background;
private int currentDisplay;
- private const int background_count = 7;
+ private const int background_count = 8;
private IBindable user;
private Bindable skin;
private Bindable source;
diff --git a/osu.Game/Screens/Edit/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/BindableBeatDivisor.cs
index ffa4f01e75..4b0726658f 100644
--- a/osu.Game/Screens/Edit/BindableBeatDivisor.cs
+++ b/osu.Game/Screens/Edit/BindableBeatDivisor.cs
@@ -29,10 +29,11 @@ namespace osu.Game.Screens.Edit
/// Set a divisor, updating the valid divisor range appropriately.
///
/// The intended divisor.
- public void SetArbitraryDivisor(int divisor)
+ /// Forces changing the valid divisors to a known preset.
+ public void SetArbitraryDivisor(int divisor, bool preferKnownPresets = false)
{
// If the current valid divisor range doesn't contain the proposed value, attempt to find one which does.
- if (!ValidDivisors.Value.Presets.Contains(divisor))
+ if (preferKnownPresets || !ValidDivisors.Value.Presets.Contains(divisor))
{
if (BeatDivisorPresetCollection.COMMON.Presets.Contains(divisor))
ValidDivisors.Value = BeatDivisorPresetCollection.COMMON;
diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
index b33edb9edb..da1a37d57f 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
@@ -208,11 +208,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
switch (currentType)
{
case BeatDivisorType.Common:
- beatDivisor.SetArbitraryDivisor(4);
+ beatDivisor.SetArbitraryDivisor(4, true);
break;
case BeatDivisorType.Triplets:
- beatDivisor.SetArbitraryDivisor(6);
+ beatDivisor.SetArbitraryDivisor(6, true);
break;
case BeatDivisorType.Custom:
diff --git a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs
index 4d1f81228e..b83e565e89 100644
--- a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs
@@ -4,6 +4,7 @@
#nullable disable
using System;
+using JetBrains.Annotations;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -69,6 +70,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
public override void Show() => State = Visibility.Visible;
+ [CanBeNull]
public event Action StateChanged;
public partial class BoxWithBorders : CompositeDrawable
diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs
index 7e484433f7..f321f7eeb0 100644
--- a/osu.Game/Screens/Edit/Timing/EffectSection.cs
+++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs
@@ -52,17 +52,38 @@ namespace osu.Game.Screens.Edit.Timing
protected override void OnControlPointChanged(ValueChangedEvent point)
{
- if (point.NewValue != null)
+ scrollSpeedSlider.Current.ValueChanged -= updateControlPointFromSlider;
+
+ if (point.NewValue is EffectControlPoint newEffectPoint)
{
isRebinding = true;
- kiai.Current = point.NewValue.KiaiModeBindable;
- scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable;
+ kiai.Current = newEffectPoint.KiaiModeBindable;
+ scrollSpeedSlider.Current = new BindableDouble
+ {
+ MinValue = 0.01,
+ MaxValue = 10,
+ Precision = 0.01,
+ Value = newEffectPoint.ScrollSpeedBindable.Value
+ };
+ scrollSpeedSlider.Current.ValueChanged += updateControlPointFromSlider;
+ // at this point in time the above is enough to keep the slider control in sync with reality,
+ // since undo/redo causes `OnControlPointChanged()` to fire.
+ // whenever that stops being the case, or there is a possibility that the scroll speed could be changed
+ // by something else other than this control, this code should probably be revisited to have a binding in the other direction, too.
isRebinding = false;
}
}
+ private void updateControlPointFromSlider(ValueChangedEvent scrollSpeed)
+ {
+ if (ControlPoint.Value is not EffectControlPoint effectPoint || isRebinding)
+ return;
+
+ effectPoint.ScrollSpeedBindable.Value = scrollSpeed.NewValue;
+ }
+
protected override EffectControlPoint CreatePoint()
{
var reference = Beatmap.ControlPointInfo.EffectPointAt(SelectedGroup.Value.Time);
diff --git a/osu.Game/Screens/Menu/ButtonArea.cs b/osu.Game/Screens/Menu/ButtonArea.cs
index 69ba68442f..4eb91c526f 100644
--- a/osu.Game/Screens/Menu/ButtonArea.cs
+++ b/osu.Game/Screens/Menu/ButtonArea.cs
@@ -4,6 +4,7 @@
#nullable disable
using System;
+using JetBrains.Annotations;
using osu.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -88,6 +89,7 @@ namespace osu.Game.Screens.Menu
public override void Show() => State = Visibility.Visible;
+ [CanBeNull]
public event Action StateChanged;
private partial class ButtonAreaBackground : Box, IStateful
@@ -146,6 +148,7 @@ namespace osu.Game.Screens.Menu
}
}
+ [CanBeNull]
public event Action StateChanged;
}
diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs
index a75edd1cff..decb901c32 100644
--- a/osu.Game/Screens/Menu/MainMenu.cs
+++ b/osu.Game/Screens/Menu/MainMenu.cs
@@ -280,7 +280,7 @@ namespace osu.Game.Screens.Menu
sideFlashes.Delay(FADE_IN_DURATION).FadeIn(64, Easing.InQuint);
}
- else if (!api.IsLoggedIn)
+ else if (!api.IsLoggedIn || api.State.Value == APIState.RequiresSecondFactorAuth)
{
// copy out old action to avoid accidentally capturing logo.Action in closure, causing a self-reference loop.
var previousAction = logo.Action;
diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs
index 75ef8be02e..f2e2e25fa6 100644
--- a/osu.Game/Screens/Menu/OsuLogo.cs
+++ b/osu.Game/Screens/Menu/OsuLogo.cs
@@ -10,6 +10,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
@@ -31,15 +32,13 @@ namespace osu.Game.Screens.Menu
///
public partial class OsuLogo : BeatSyncedContainer
{
- public readonly Color4 OsuPink = Color4Extensions.FromHex(@"e967a1");
-
private const double transition_length = 300;
///
/// The osu! logo sprite has a shadow included in its texture.
/// This adjustment vector is used to match the precise edge of the border of the logo.
///
- public static readonly Vector2 SCALE_ADJUST = new Vector2(0.96f);
+ public static readonly Vector2 SCALE_ADJUST = new Vector2(0.94f);
private readonly Sprite logo;
private readonly CircularContainer logoContainer;
@@ -58,7 +57,7 @@ namespace osu.Game.Screens.Menu
private Sample sampleDownbeat;
private readonly Container colourAndTriangles;
- private readonly Triangles triangles;
+ private readonly TrianglesV2 triangles;
///
/// Return value decides whether the logo should play its own sample for the click action.
@@ -184,13 +183,16 @@ namespace osu.Game.Screens.Menu
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = OsuPink,
+ Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex(@"ff66ab"), Color4Extensions.FromHex(@"cc5289")),
},
- triangles = new Triangles
+ triangles = new TrianglesV2
{
- TriangleScale = 4,
- ColourLight = Color4Extensions.FromHex(@"ff7db7"),
- ColourDark = Color4Extensions.FromHex(@"de5b95"),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Thickness = 0.009f,
+ ScaleAdjust = 3,
+ SpawnRatio = 1.4f,
+ Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex(@"ff66ab"), Color4Extensions.FromHex(@"b6346f")),
RelativeSizeAxes = Axes.Both,
},
}
@@ -484,5 +486,12 @@ namespace osu.Game.Screens.Menu
defaultProxyTarget.Add(this);
defaultProxyTarget.Add(proxy = CreateProxy());
}
+
+ public void ChangeAnchor(Anchor anchor)
+ {
+ var previousAnchor = AnchorPosition;
+ Anchor = anchor;
+ Position -= AnchorPosition - previousAnchor;
+ }
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
index e892f9280f..cb27d1ee61 100644
--- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Development;
@@ -19,6 +20,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
public partial class RoomManager : Component, IRoomManager
{
+ [CanBeNull]
public event Action RoomsUpdated;
private readonly BindableList rooms = new BindableList();
diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
index f35b205bc4..4c0219eff5 100644
--- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
@@ -509,7 +509,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
private void cancelTrackLooping()
{
- var track = Beatmap?.Value?.Track;
+ var track = Beatmap.Value?.Track;
if (track != null)
track.Looping = false;
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
index d9043df1d5..c5c536eae6 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
@@ -67,6 +67,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (!LoadedBeatmapSuccessfully)
return;
+ ScoreProcessor.ApplyNewJudgementsWhenFailed = true;
+
LoadComponentAsync(new GameplayChatDisplay(Room)
{
Expanded = { BindTarget = LeaderboardExpandedState },
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
index aa72394ac9..6a1924dea2 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[Resolved]
private RulesetStore rulesets { get; set; }
- public PlaylistsResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true)
+ public PlaylistsResultsScreen([CanBeNull] ScoreInfo score, long roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true)
: base(score, allowRetry, allowWatchingReplay)
{
this.roomId = roomId;
diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs
index f719ef67c9..2e8f85423d 100644
--- a/osu.Game/Screens/OsuScreen.cs
+++ b/osu.Game/Screens/OsuScreen.cs
@@ -250,9 +250,12 @@ namespace osu.Game.Screens
{
logo.Action = null;
logo.FadeOut(300, Easing.OutQuint);
- logo.Anchor = Anchor.TopLeft;
+
logo.Origin = Anchor.Centre;
+
+ logo.ChangeAnchor(Anchor.TopLeft);
logo.RelativePositionAxes = Axes.Both;
+
logo.Triangles = true;
logo.Ripple = true;
}
diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs
index 348327d710..44b9fb3123 100644
--- a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs
+++ b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play.HUD
public bool UsesFixedAnchor { get; set; }
- protected override LocalisableString FormatCount(long count) => count.ToLocalisableString();
+ protected override LocalisableString FormatCount(long count) => count.ToString();
protected override IHasText CreateText() => scoreText = new ArgonScoreTextComponent(Anchor.TopRight, BeatmapsetsStrings.ShowScoreboardHeadersScore.ToUpper())
{
diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs
index cb38854bca..7db3f9fd3c 100644
--- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs
+++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs
@@ -114,12 +114,7 @@ namespace osu.Game.Screens.Play.HUD
protected override void UpdateProgress(double progress, bool isIntro)
{
- bar.TrackTime = GameplayClock.CurrentTime;
-
- if (isIntro)
- bar.CurrentTime = 0;
- else
- bar.CurrentTime = FrameStableClock.CurrentTime;
+ bar.Progress = isIntro ? 0 : progress;
}
}
}
diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs
index beaee0e9ee..7a7870a775 100644
--- a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs
+++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs
@@ -3,96 +3,59 @@
using System;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
-using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
- public partial class ArgonSongProgressBar : SliderBar
+ public partial class ArgonSongProgressBar : SongProgressBar
{
- public Action? OnSeek { get; set; }
-
// Parent will handle restricting the area of valid input.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
private readonly float barHeight;
private readonly RoundedBar playfieldBar;
- private readonly RoundedBar catchupBar;
+ private readonly RoundedBar audioBar;
private readonly Box background;
private readonly ColourInfo mainColour;
private ColourInfo catchUpColour;
- public double StartTime
- {
- private get => CurrentNumber.MinValue;
- set => CurrentNumber.MinValue = value;
- }
+ public double Progress { get; set; }
- public double EndTime
- {
- private get => CurrentNumber.MaxValue;
- set => CurrentNumber.MaxValue = value;
- }
-
- public double CurrentTime
- {
- private get => CurrentNumber.Value;
- set => CurrentNumber.Value = value;
- }
-
- public double TrackTime
- {
- private get => currentTrackTime.Value;
- set => currentTrackTime.Value = value;
- }
-
- private double length => EndTime - StartTime;
-
- private readonly BindableNumber currentTrackTime;
-
- public bool Interactive { get; set; }
+ private double trackTime => (EndTime - StartTime) * Progress;
public ArgonSongProgressBar(float barHeight)
{
- currentTrackTime = new BindableDouble();
- setupAlternateValue();
-
- StartTime = 0;
- EndTime = 1;
-
RelativeSizeAxes = Axes.X;
Height = this.barHeight = barHeight;
CornerRadius = 5;
Masking = true;
- Children = new Drawable[]
+ InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Colour = OsuColour.Gray(0.2f),
+ Depth = float.MaxValue,
},
- catchupBar = new RoundedBar
+ audioBar = new RoundedBar
{
Name = "Audio bar",
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
CornerRadius = 5,
- AlwaysPresent = true,
RelativeSizeAxes = Axes.Both
},
playfieldBar = new RoundedBar
@@ -107,24 +70,6 @@ namespace osu.Game.Screens.Play.HUD
};
}
- private void setupAlternateValue()
- {
- CurrentNumber.MaxValueChanged += v => currentTrackTime.MaxValue = v;
- CurrentNumber.MinValueChanged += v => currentTrackTime.MinValue = v;
- CurrentNumber.PrecisionChanged += v => currentTrackTime.Precision = v;
- }
-
- private float normalizedReference
- {
- get
- {
- if (EndTime - StartTime == 0)
- return 1;
-
- return (float)((TrackTime - StartTime) / length);
- }
- }
-
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@@ -153,47 +98,28 @@ namespace osu.Game.Screens.Play.HUD
base.OnHoverLost(e);
}
- protected override void UpdateValue(float value)
- {
- // Handled in Update
- }
-
protected override void Update()
{
base.Update();
- playfieldBar.Length = (float)Interpolation.Lerp(playfieldBar.Length, NormalizedValue, Math.Clamp(Time.Elapsed / 40, 0, 1));
- catchupBar.Length = (float)Interpolation.Lerp(catchupBar.Length, normalizedReference, Math.Clamp(Time.Elapsed / 40, 0, 1));
+ playfieldBar.Length = (float)Interpolation.Lerp(playfieldBar.Length, Progress, Math.Clamp(Time.Elapsed / 40, 0, 1));
+ audioBar.Length = (float)Interpolation.Lerp(audioBar.Length, AudioProgress, Math.Clamp(Time.Elapsed / 40, 0, 1));
- if (TrackTime < CurrentTime)
- ChangeChildDepth(catchupBar, -1);
+ if (trackTime > AudioTime)
+ ChangeInternalChildDepth(audioBar, -1);
else
- ChangeChildDepth(catchupBar, 0);
+ ChangeInternalChildDepth(audioBar, 1);
- float timeDelta = (float)(Math.Abs(CurrentTime - TrackTime));
+ float timeDelta = (float)Math.Abs(AudioTime - trackTime);
const float colour_transition_threshold = 20000;
- catchupBar.AccentColour = Interpolation.ValueAt(
+ audioBar.AccentColour = Interpolation.ValueAt(
Math.Min(timeDelta, colour_transition_threshold),
mainColour,
catchUpColour,
0, colour_transition_threshold,
Easing.OutQuint);
-
- catchupBar.Alpha = Math.Max(1, catchupBar.Length);
- }
-
- private ScheduledDelegate? scheduledSeek;
-
- protected override void OnUserChange(double value)
- {
- scheduledSeek?.Cancel();
- scheduledSeek = Schedule(() =>
- {
- if (Interactive)
- OnSeek?.Invoke(value);
- });
}
private partial class RoundedBar : Container
diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs
index 48809796f3..f01c11855c 100644
--- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs
+++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs
@@ -98,12 +98,7 @@ namespace osu.Game.Screens.Play.HUD
protected override void UpdateProgress(double progress, bool isIntro)
{
- bar.CurrentTime = GameplayClock.CurrentTime;
-
- if (isIntro)
- graph.Progress = 0;
- else
- graph.Progress = (int)(graph.ColumnCount * progress);
+ graph.Progress = isIntro ? 0 : (int)(graph.ColumnCount * progress);
}
protected override void Update()
diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgressBar.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgressBar.cs
index 0e16067dcc..d5a6a75793 100644
--- a/osu.Game/Screens/Play/HUD/DefaultSongProgressBar.cs
+++ b/osu.Game/Screens/Play/HUD/DefaultSongProgressBar.cs
@@ -7,71 +7,27 @@ using osuTK.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.UserInterface;
using osu.Framework.Utils;
-using osu.Framework.Threading;
namespace osu.Game.Screens.Play.HUD
{
- public partial class DefaultSongProgressBar : SliderBar
+ public partial class DefaultSongProgressBar : SongProgressBar
{
- ///
- /// Action which is invoked when a seek is requested, with the proposed millisecond value for the seek operation.
- ///
- public Action? OnSeek { get; set; }
-
- ///
- /// Whether the progress bar should allow interaction, ie. to perform seek operations.
- ///
- public bool Interactive
- {
- get => showHandle;
- set
- {
- if (value == showHandle)
- return;
-
- showHandle = value;
-
- handleBase.FadeTo(showHandle ? 1 : 0, 200);
- }
- }
-
public Color4 FillColour
{
set => fill.Colour = value;
}
- public double StartTime
- {
- set => CurrentNumber.MinValue = value;
- }
-
- public double EndTime
- {
- set => CurrentNumber.MaxValue = value;
- }
-
- public double CurrentTime
- {
- set => CurrentNumber.Value = value;
- }
-
private readonly Box fill;
private readonly Container handleBase;
private readonly Container handleContainer;
- private bool showHandle;
-
public DefaultSongProgressBar(float barHeight, float handleBarHeight, Vector2 handleSize)
{
- CurrentNumber.MinValue = 0;
- CurrentNumber.MaxValue = 1;
-
RelativeSizeAxes = Axes.X;
Height = barHeight + handleBarHeight + handleSize.Y;
- Children = new Drawable[]
+ InternalChildren = new Drawable[]
{
new Box
{
@@ -130,9 +86,14 @@ namespace osu.Game.Screens.Play.HUD
};
}
- protected override void UpdateValue(float value)
+ public override bool Interactive
{
- // handled in update
+ get => base.Interactive;
+ set
+ {
+ base.Interactive = value;
+ handleBase.FadeTo(value ? 1 : 0, 200);
+ }
}
protected override void Update()
@@ -140,22 +101,10 @@ namespace osu.Game.Screens.Play.HUD
base.Update();
handleBase.Height = Height - handleContainer.Height;
- float newX = (float)Interpolation.Lerp(handleBase.X, NormalizedValue * UsableWidth, Math.Clamp(Time.Elapsed / 40, 0, 1));
+ float newX = (float)Interpolation.Lerp(handleBase.X, AudioProgress * DrawWidth, Math.Clamp(Time.Elapsed / 40, 0, 1));
fill.Width = newX;
handleBase.X = newX;
}
-
- private ScheduledDelegate? scheduledSeek;
-
- protected override void OnUserChange(double value)
- {
- scheduledSeek?.Cancel();
- scheduledSeek = Schedule(() =>
- {
- if (showHandle)
- OnSeek?.Invoke(value);
- });
- }
}
}
diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs
index 1cf3d25dad..a260156595 100644
--- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs
+++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs
@@ -109,7 +109,7 @@ namespace osu.Game.Screens.Play.HUD
protected override bool OnMouseMove(MouseMoveEvent e)
{
- positionalAdjust = Vector2.Distance(e.MousePosition, button.ToSpaceOfOtherDrawable(button.DrawRectangle.Centre, Parent)) / 100;
+ positionalAdjust = Vector2.Distance(e.MousePosition, button.ToSpaceOfOtherDrawable(button.DrawRectangle.Centre, Parent!)) / 100;
return base.OnMouseMove(e);
}
diff --git a/osu.Game/Screens/Play/HUD/SongProgress.cs b/osu.Game/Screens/Play/HUD/SongProgress.cs
index 4391193df8..296306ec89 100644
--- a/osu.Game/Screens/Play/HUD/SongProgress.cs
+++ b/osu.Game/Screens/Play/HUD/SongProgress.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -70,7 +71,13 @@ namespace osu.Game.Screens.Play.HUD
protected double LastHitTime { get; private set; }
+ ///
+ /// Called every update frame with current progress information.
+ ///
+ /// Current (visual) progress through the beatmap (0..1).
+ /// If true, progress is (0..1) through the intro.
protected abstract void UpdateProgress(double progress, bool isIntro);
+
protected virtual void UpdateObjects(IEnumerable objects) { }
[BackgroundDependencyLoader]
@@ -96,7 +103,7 @@ namespace osu.Game.Screens.Play.HUD
if (objects == null)
return;
- double currentTime = FrameStableClock.CurrentTime;
+ double currentTime = Math.Min(FrameStableClock.CurrentTime, LastHitTime);
bool isInIntro = currentTime < FirstHitTime;
diff --git a/osu.Game/Screens/Play/HUD/SongProgressBar.cs b/osu.Game/Screens/Play/HUD/SongProgressBar.cs
new file mode 100644
index 0000000000..40c4e587b9
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/SongProgressBar.cs
@@ -0,0 +1,97 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Events;
+using osu.Framework.Threading;
+using osuTK;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ public abstract partial class SongProgressBar : CompositeDrawable
+ {
+ ///
+ /// The current seek position of the audio, on a (0..1) range.
+ /// This is generally the seek target, which will eventually match the gameplay clock when it catches up.
+ ///
+ protected double AudioProgress => length == 0 ? 1 : AudioTime / length;
+
+ ///
+ /// The current (non-frame-stable) audio time.
+ ///
+ protected double AudioTime => Math.Clamp(GameplayClock.CurrentTime - StartTime, 0.0, length);
+
+ [Resolved]
+ protected IGameplayClock GameplayClock { get; private set; } = null!;
+
+ ///
+ /// Action which is invoked when a seek is requested, with the proposed millisecond value for the seek operation.
+ ///
+ public Action? OnSeek { get; set; }
+
+ ///
+ /// Whether the progress bar should allow interaction, ie. to perform seek operations.
+ ///
+ public virtual bool Interactive { get; set; }
+
+ public double StartTime { get; set; }
+
+ public double EndTime { get; set; } = 1.0;
+
+ private double length => EndTime - StartTime;
+
+ private bool handleClick;
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ handleClick = true;
+ return base.OnMouseDown(e);
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ if (handleClick)
+ handleMouseInput(e);
+
+ return true;
+ }
+
+ protected override void OnDrag(DragEvent e)
+ {
+ handleMouseInput(e);
+ }
+
+ protected override bool OnDragStart(DragStartEvent e)
+ {
+ Vector2 posDiff = e.MouseDownPosition - e.MousePosition;
+
+ if (Math.Abs(posDiff.X) < Math.Abs(posDiff.Y))
+ {
+ handleClick = false;
+ return false;
+ }
+
+ handleMouseInput(e);
+ return true;
+ }
+
+ private void handleMouseInput(UIEvent e)
+ {
+ if (!Interactive)
+ return;
+
+ double relativeX = Math.Clamp(ToLocalSpace(e.ScreenSpaceMousePosition).X / DrawWidth, 0, 1);
+ onUserChange(StartTime + (EndTime - StartTime) * relativeX);
+ }
+
+ private ScheduledDelegate? scheduledSeek;
+
+ private void onUserChange(double value)
+ {
+ scheduledSeek?.Cancel();
+ scheduledSeek = Schedule(() => OnSeek?.Invoke(value));
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index b5482f2a5b..32ebb82f15 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -259,13 +258,12 @@ namespace osu.Game.Screens.Play
Vector2? highestBottomScreenSpace = null;
- // LINQ cast can be removed when IDrawable interface includes Anchor / RelativeSizeAxes.
- foreach (var element in mainComponents.Components.Cast())
+ foreach (var element in mainComponents.Components)
processDrawable(element);
if (rulesetComponents != null)
{
- foreach (var element in rulesetComponents.Components.Cast())
+ foreach (var element in rulesetComponents.Components)
processDrawable(element);
}
@@ -284,33 +282,36 @@ namespace osu.Game.Screens.Play
else
bottomRightElements.Y = 0;
- void processDrawable(Drawable element)
+ void processDrawable(ISerialisableDrawable element)
{
+ // Cast can be removed when IDrawable interface includes Anchor / RelativeSizeAxes.
+ Drawable drawable = (Drawable)element;
+
// for now align some top components with the bottom-edge of the lowest top-anchored hud element.
- if (element.Anchor.HasFlagFast(Anchor.y0))
+ if (drawable.Anchor.HasFlagFast(Anchor.y0))
{
// health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area.
if (element is LegacyHealthDisplay)
return;
- float bottom = element.ScreenSpaceDrawQuad.BottomRight.Y;
+ float bottom = drawable.ScreenSpaceDrawQuad.BottomRight.Y;
- bool isRelativeX = element.RelativeSizeAxes == Axes.X;
+ bool isRelativeX = drawable.RelativeSizeAxes == Axes.X;
- if (element.Anchor.HasFlagFast(Anchor.TopRight) || isRelativeX)
+ if (drawable.Anchor.HasFlagFast(Anchor.TopRight) || isRelativeX)
{
if (lowestTopScreenSpaceRight == null || bottom > lowestTopScreenSpaceRight.Value)
lowestTopScreenSpaceRight = bottom;
}
- if (element.Anchor.HasFlagFast(Anchor.TopLeft) || isRelativeX)
+ if (drawable.Anchor.HasFlagFast(Anchor.TopLeft) || isRelativeX)
{
if (lowestTopScreenSpaceLeft == null || bottom > lowestTopScreenSpaceLeft.Value)
lowestTopScreenSpaceLeft = bottom;
}
}
// and align bottom-right components with the top-edge of the highest bottom-anchored hud element.
- else if (element.Anchor.HasFlagFast(Anchor.BottomRight) || (element.Anchor.HasFlagFast(Anchor.y2) && element.RelativeSizeAxes == Axes.X))
+ else if (drawable.Anchor.HasFlagFast(Anchor.BottomRight) || (drawable.Anchor.HasFlagFast(Anchor.y2) && drawable.RelativeSizeAxes == Axes.X))
{
var topLeft = element.ScreenSpaceDrawQuad.TopLeft;
if (highestBottomScreenSpace == null || topLeft.Y < highestBottomScreenSpace.Value.Y)
diff --git a/osu.Game/Screens/Play/SpectatorResultsScreen.cs b/osu.Game/Screens/Play/SpectatorResultsScreen.cs
index 001d3b4bbc..393cbddb34 100644
--- a/osu.Game/Screens/Play/SpectatorResultsScreen.cs
+++ b/osu.Game/Screens/Play/SpectatorResultsScreen.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Screens.Play
private void userBeganPlaying(int userId, SpectatorState state)
{
- if (userId == Score.UserID)
+ if (userId == Score?.UserID)
{
Schedule(() =>
{
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index 171ceea84f..c8e84f1961 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -132,7 +132,18 @@ namespace osu.Game.Screens.Play
if (string.IsNullOrEmpty(exception.Message))
Logger.Error(exception, "Failed to retrieve a score submission token.");
else
- Logger.Log($"You are not able to submit a score: {exception.Message}", level: LogLevel.Important);
+ {
+ switch (exception.Message)
+ {
+ case "expired token":
+ Logger.Log("Score submission failed because your system clock is set incorrectly. Please check your system time, date and timezone.", level: LogLevel.Important);
+ break;
+
+ default:
+ Logger.Log($"You are not able to submit a score: {exception.Message}", level: LogLevel.Important);
+ break;
+ }
+ }
Schedule(() =>
{
diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
index e7e54d0fae..0aff98df2b 100644
--- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
@@ -417,7 +417,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
accuracyCircle
.FillTo(accuracyS - NOTCH_WIDTH_PERCENTAGE / 2 - visual_alignment_offset, 70, Easing.OutQuint);
- badges.Single(b => b.Rank == ScoreRank.S)
+ badges.Single(b => b.Rank == getRank(ScoreRank.S))
.FadeOut(70, Easing.OutQuint);
}
}
diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs
index 22509b2cea..22c1e26d43 100644
--- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs
@@ -5,10 +5,11 @@
using System;
using System.Threading;
+using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Extensions;
using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
@@ -32,7 +33,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
}
[BackgroundDependencyLoader]
- private void load(ScorePerformanceCache performanceCache)
+ private void load(BeatmapDifficultyCache difficultyCache, CancellationToken? cancellationToken)
{
if (score.PP.HasValue)
{
@@ -40,8 +41,19 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
}
else
{
- performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token)
- .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()?.Total)), cancellationTokenSource.Token);
+ Task.Run(async () =>
+ {
+ var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, cancellationToken ?? default).ConfigureAwait(false);
+ var performanceCalculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator();
+
+ // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value.
+ if (attributes?.Attributes == null || performanceCalculator == null)
+ return;
+
+ var result = await performanceCalculator.CalculateAsync(score, attributes.Value.Attributes, cancellationToken ?? default).ConfigureAwait(false);
+
+ Schedule(() => setPerformanceValue(result.Total));
+ }, cancellationToken ?? default);
}
}
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index 697d62ad6e..82dade40eb 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -45,6 +46,7 @@ namespace osu.Game.Screens.Ranking
public readonly Bindable SelectedScore = new Bindable();
+ [CanBeNull]
public readonly ScoreInfo Score;
protected ScorePanelList ScorePanelList { get; private set; }
@@ -69,7 +71,7 @@ namespace osu.Game.Screens.Ranking
private Sample popInSample;
- protected ResultsScreen(ScoreInfo score, bool allowRetry, bool allowWatchingReplay = true)
+ protected ResultsScreen([CanBeNull] ScoreInfo score, bool allowRetry, bool allowWatchingReplay = true)
{
Score = score;
this.allowRetry = allowRetry;
@@ -275,6 +277,11 @@ namespace osu.Game.Screens.Ranking
if (base.OnExiting(e))
return true;
+ // This is a stop-gap safety against components holding references to gameplay after exiting the gameplay flow.
+ // Right now, HitEvents are only used up to the results screen. If this changes in the future we need to remove
+ // HitObject references from HitEvent.
+ Score?.HitEvents.Clear();
+
this.FadeOut(100);
return false;
}
diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
index da08a26a58..22d631e137 100644
--- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -45,12 +46,16 @@ namespace osu.Game.Screens.Ranking
{
base.LoadComplete();
+ Debug.Assert(Score != null);
+
if (ShowUserStatistics)
statisticsSubscription = soloStatisticsWatcher.RegisterForStatisticsUpdateAfter(Score, update => statisticsUpdate.Value = update);
}
protected override StatisticsPanel CreateStatisticsPanel()
{
+ Debug.Assert(Score != null);
+
if (ShowUserStatistics)
{
return new SoloStatisticsPanel(Score)
@@ -64,6 +69,8 @@ namespace osu.Game.Screens.Ranking
protected override APIRequest? FetchScores(Action>? scoresCallback)
{
+ Debug.Assert(Score != null);
+
if (Score.BeatmapInfo!.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending)
return null;
diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs
index ee0ce6183d..8b13f0951c 100644
--- a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs
+++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs
@@ -39,9 +39,6 @@ namespace osu.Game.Screens.Ranking.Statistics
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
- [Resolved]
- private ScorePerformanceCache performanceCache { get; set; }
-
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
@@ -148,7 +145,7 @@ namespace osu.Game.Screens.Ranking.Statistics
spinner.Show();
- new PerformanceBreakdownCalculator(playableBeatmap, difficultyCache, performanceCache)
+ new PerformanceBreakdownCalculator(playableBeatmap, difficultyCache)
.CalculateAsync(score, cancellationTokenSource.Token)
.ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely())));
}
diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs
index 69782c25bb..5685910c0a 100644
--- a/osu.Game/Screens/Select/FooterButtonMods.cs
+++ b/osu.Game/Screens/Select/FooterButtonMods.cs
@@ -1,17 +1,17 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Screens.Play.HUD;
using osu.Game.Rulesets.Mods;
using System.Collections.Generic;
using System.Linq;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.LocalisationExtensions;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Configuration;
using osu.Game.Graphics;
@@ -19,6 +19,7 @@ using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
using osu.Game.Input.Bindings;
+using osu.Game.Localisation;
using osu.Game.Utils;
namespace osu.Game.Screens.Select
@@ -31,26 +32,26 @@ namespace osu.Game.Screens.Select
set => modDisplay.Current = value;
}
- protected readonly OsuSpriteText MultiplierText;
+ protected OsuSpriteText MultiplierText { get; private set; } = null!;
+ protected Container UnrankedBadge { get; private set; } = null!;
+
private readonly ModDisplay modDisplay;
+
+ private ModSettingChangeTracker? modSettingChangeTracker;
+
private Color4 lowMultiplierColour;
private Color4 highMultiplierColour;
public FooterButtonMods()
{
- ButtonContentContainer.Add(modDisplay = new ModDisplay
+ // must be created in ctor for correct operation of `Current`.
+ modDisplay = new ModDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(0.8f),
ExpansionMode = ExpansionMode.AlwaysContracted,
- });
- ButtonContentContainer.Add(MultiplierText = new OsuSpriteText
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Font = OsuFont.GetFont(weight: FontWeight.Bold),
- });
+ };
}
[BackgroundDependencyLoader]
@@ -62,10 +63,43 @@ namespace osu.Game.Screens.Select
highMultiplierColour = colours.Green;
Text = @"mods";
Hotkey = GlobalAction.ToggleModSelection;
- }
- [CanBeNull]
- private ModSettingChangeTracker modSettingChangeTracker;
+ ButtonContentContainer.AddRange(new Drawable[]
+ {
+ modDisplay,
+ MultiplierText = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Font = OsuFont.GetFont(weight: FontWeight.Bold),
+ },
+ UnrankedBadge = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Circle
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = colours.Yellow,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = colours.Gray2,
+ Padding = new MarginPadding(5),
+ UseFullGlyphHeight = false,
+ Text = ModSelectOverlayStrings.Unranked.ToLower()
+ }
+ }
+ },
+ });
+ }
protected override void LoadComplete()
{
@@ -101,6 +135,9 @@ namespace osu.Game.Screens.Select
modDisplay.FadeIn();
else
modDisplay.FadeOut();
+
+ bool anyUnrankedMods = Current.Value?.Any(m => !m.Ranked) == true;
+ UnrankedBadge.FadeTo(anyUnrankedMods ? 1 : 0);
});
}
}
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index bf1724995a..a603934a9d 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -642,7 +642,10 @@ namespace osu.Game.Screens.Select
{
base.LogoArriving(logo, resuming);
- Vector2 position = new Vector2(0.95f, 0.96f);
+ logo.RelativePositionAxes = Axes.None;
+ logo.ChangeAnchor(Anchor.BottomRight);
+
+ Vector2 position = new Vector2(-76, -36);
if (logo.Alpha > 0.8f)
{
diff --git a/osu.Game/Skinning/LegacyJudgementPieceOld.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs
index a9f68bd378..c8630b54a6 100644
--- a/osu.Game/Skinning/LegacyJudgementPieceOld.cs
+++ b/osu.Game/Skinning/LegacyJudgementPieceOld.cs
@@ -50,11 +50,7 @@ namespace osu.Game.Skinning
// legacy judgements don't play any transforms if they are an animation.... UNLESS they are the temporary displayed judgement from new piece.
if (animation?.FrameCount > 1 && !forceTransforms)
- {
- if (isMissedTick())
- applyMissedTickScaling();
return;
- }
if (result.IsMiss())
{
@@ -66,7 +62,7 @@ namespace osu.Game.Skinning
this.ScaleTo(1.2f);
this.ScaleTo(1f, 100, Easing.In);
- this.FadeOutFromOne(400);
+ this.Delay(fade_out_delay / 2).FadeOut(fade_out_length);
}
else
{
@@ -102,12 +98,6 @@ namespace osu.Game.Skinning
private bool isMissedTick() => result.IsMiss() && result != HitResult.Miss;
- private void applyMissedTickScaling()
- {
- this.ScaleTo(0.6f);
- this.ScaleTo(0.3f, 100, Easing.In);
- }
-
public Drawable GetAboveHitObjectsProxiedContent() => CreateProxy();
}
}
diff --git a/osu.Game/Online/Notifications/NotificationsClient.cs b/osu.Game/Tests/PollingChatClient.cs
similarity index 59%
rename from osu.Game/Online/Notifications/NotificationsClient.cs
rename to osu.Game/Tests/PollingChatClient.cs
index 5762e0e588..eb29b35c1d 100644
--- a/osu.Game/Online/Notifications/NotificationsClient.cs
+++ b/osu.Game/Tests/PollingChatClient.cs
@@ -6,34 +6,39 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Chat;
-namespace osu.Game.Online.Notifications
+namespace osu.Game.Tests
{
- ///
- /// An abstract client which receives notification-related events (chat/notifications).
- ///
- public abstract class NotificationsClient : PersistentEndpointClient
+ public class PollingChatClient : PersistentEndpointClient
{
- public Action? ChannelJoined;
- public Action? ChannelParted;
- public Action>? NewMessages;
- public Action? PresenceReceived;
+ public event Action? ChannelJoined;
+ public event Action>? NewMessages;
+ public event Action? PresenceReceived;
- protected readonly IAPIProvider API;
+ private readonly IAPIProvider api;
private long lastMessageId;
- protected NotificationsClient(IAPIProvider api)
+ public PollingChatClient(IAPIProvider api)
{
- API = api;
+ this.api = api;
}
public override Task ConnectAsync(CancellationToken cancellationToken)
{
- API.Queue(CreateInitialFetchRequest(0));
+ Task.Run(async () =>
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ await api.PerformAsync(CreateInitialFetchRequest()).ConfigureAwait(true);
+ await Task.Delay(1000, cancellationToken).ConfigureAwait(true);
+ }
+ }, cancellationToken);
+
return Task.CompletedTask;
}
@@ -46,11 +51,11 @@ namespace osu.Game.Online.Notifications
if (updates?.Presence != null)
{
foreach (var channel in updates.Presence)
- HandleChannelJoined(channel);
+ handleChannelJoined(channel);
//todo: handle left channels
- HandleMessages(updates.Messages);
+ handleMessages(updates.Messages);
}
PresenceReceived?.Invoke();
@@ -59,15 +64,13 @@ namespace osu.Game.Online.Notifications
return fetchReq;
}
- protected void HandleChannelJoined(Channel channel)
+ private void handleChannelJoined(Channel channel)
{
channel.Joined.Value = true;
ChannelJoined?.Invoke(channel);
}
- protected void HandleChannelParted(Channel channel) => ChannelParted?.Invoke(channel);
-
- protected void HandleMessages(List? messages)
+ private void handleMessages(List? messages)
{
if (messages == null)
return;
diff --git a/osu.Game/Tests/PollingNotificationsClient.cs b/osu.Game/Tests/PollingNotificationsClient.cs
deleted file mode 100644
index 450c763170..0000000000
--- a/osu.Game/Tests/PollingNotificationsClient.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Threading;
-using System.Threading.Tasks;
-using osu.Game.Online.API;
-using osu.Game.Online.Notifications;
-
-namespace osu.Game.Tests
-{
- ///
- /// A notifications client which polls for new messages every second.
- ///
- public class PollingNotificationsClient : NotificationsClient
- {
- public PollingNotificationsClient(IAPIProvider api)
- : base(api)
- {
- }
-
- public override Task ConnectAsync(CancellationToken cancellationToken)
- {
- Task.Run(async () =>
- {
- while (!cancellationToken.IsCancellationRequested)
- {
- await API.PerformAsync(CreateInitialFetchRequest()).ConfigureAwait(true);
- await Task.Delay(1000, cancellationToken).ConfigureAwait(true);
- }
- }, cancellationToken);
-
- return Task.CompletedTask;
- }
- }
-}
diff --git a/osu.Game/Tests/PollingNotificationsClientConnector.cs b/osu.Game/Tests/PollingNotificationsClientConnector.cs
deleted file mode 100644
index 823fc9d157..0000000000
--- a/osu.Game/Tests/PollingNotificationsClientConnector.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Threading;
-using System.Threading.Tasks;
-using osu.Game.Online.API;
-using osu.Game.Online.Notifications;
-
-namespace osu.Game.Tests
-{
- ///
- /// A connector for s that poll for new messages.
- ///
- public class PollingNotificationsClientConnector : NotificationsClientConnector
- {
- public PollingNotificationsClientConnector(IAPIProvider api)
- : base(api)
- {
- }
-
- protected override Task BuildNotificationClientAsync(CancellationToken cancellationToken)
- => Task.FromResult((NotificationsClient)new PollingNotificationsClient(API));
- }
-}
diff --git a/osu.Game/Tests/TestChatClientConnector.cs b/osu.Game/Tests/TestChatClientConnector.cs
new file mode 100644
index 0000000000..40e15b5ef5
--- /dev/null
+++ b/osu.Game/Tests/TestChatClientConnector.cs
@@ -0,0 +1,49 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using osu.Game.Online;
+using osu.Game.Online.API;
+using osu.Game.Online.Chat;
+
+namespace osu.Game.Tests
+{
+ public class TestChatClientConnector : PersistentEndpointClientConnector, IChatClient
+ {
+ public event Action? ChannelJoined;
+
+ public event Action? ChannelParted
+ {
+ add { }
+ remove { }
+ }
+
+ public event Action>? NewMessages;
+ public event Action? PresenceReceived;
+
+ public void RequestPresence()
+ {
+ // don't really need to do anything special if we poll every second anyway.
+ }
+
+ public TestChatClientConnector(IAPIProvider api)
+ : base(api)
+ {
+ Start();
+ }
+
+ protected sealed override Task BuildConnectionAsync(CancellationToken cancellationToken)
+ {
+ var client = new PollingChatClient(API);
+
+ client.ChannelJoined += c => ChannelJoined?.Invoke(c);
+ client.NewMessages += m => NewMessages?.Invoke(m);
+ client.PresenceReceived += () => PresenceReceived?.Invoke();
+
+ return Task.FromResult(client);
+ }
+ }
+}
diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
index 93c6e72aa2..80c69db8b1 100644
--- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public TestMultiplayerClient MultiplayerClient => OnlinePlayDependencies.MultiplayerClient;
public new TestMultiplayerRoomManager RoomManager => OnlinePlayDependencies.RoomManager;
- public TestSpectatorClient SpectatorClient => OnlinePlayDependencies?.SpectatorClient;
+ public TestSpectatorClient SpectatorClient => OnlinePlayDependencies.SpectatorClient;
protected new MultiplayerTestSceneDependencies OnlinePlayDependencies => (MultiplayerTestSceneDependencies)base.OnlinePlayDependencies;
diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs
index 87488710a7..eebc3503bc 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -22,23 +20,23 @@ namespace osu.Game.Tests.Visual.OnlinePlay
///
public abstract partial class OnlinePlayTestScene : ScreenTestScene, IOnlinePlayTestSceneDependencies
{
- public Bindable SelectedRoom => OnlinePlayDependencies?.SelectedRoom;
- public IRoomManager RoomManager => OnlinePlayDependencies?.RoomManager;
- public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies?.OngoingOperationTracker;
- public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies?.AvailabilityTracker;
- public TestUserLookupCache UserLookupCache => OnlinePlayDependencies?.UserLookupCache;
- public BeatmapLookupCache BeatmapLookupCache => OnlinePlayDependencies?.BeatmapLookupCache;
+ public Bindable SelectedRoom => OnlinePlayDependencies.SelectedRoom;
+ public IRoomManager RoomManager => OnlinePlayDependencies.RoomManager;
+ public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies.OngoingOperationTracker;
+ public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies.AvailabilityTracker;
+ public TestUserLookupCache UserLookupCache => OnlinePlayDependencies.UserLookupCache;
+ public BeatmapLookupCache BeatmapLookupCache => OnlinePlayDependencies.BeatmapLookupCache;
///
/// All dependencies required for online play components and screens.
///
- protected OnlinePlayTestSceneDependencies OnlinePlayDependencies => dependencies?.OnlinePlayDependencies;
+ protected OnlinePlayTestSceneDependencies OnlinePlayDependencies => dependencies.OnlinePlayDependencies!;
protected override Container Content => content;
private readonly Container content;
private readonly Container drawableDependenciesContainer;
- private DelegatedDependencyContainer dependencies;
+ private DelegatedDependencyContainer dependencies = null!;
protected OnlinePlayTestScene()
{
@@ -50,10 +48,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
}
protected sealed override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
- {
- dependencies = new DelegatedDependencyContainer(base.CreateChildDependencies(parent));
- return dependencies;
- }
+ => dependencies = new DelegatedDependencyContainer(base.CreateChildDependencies(parent));
public override void SetUpSteps()
{
@@ -62,9 +57,9 @@ namespace osu.Game.Tests.Visual.OnlinePlay
AddStep("setup dependencies", () =>
{
// Reset the room dependencies to a fresh state.
- drawableDependenciesContainer.Clear();
dependencies.OnlinePlayDependencies = CreateOnlinePlayDependencies();
- drawableDependenciesContainer.AddRange(OnlinePlayDependencies.DrawableComponents);
+ drawableDependenciesContainer.Clear();
+ drawableDependenciesContainer.AddRange(dependencies.OnlinePlayDependencies.DrawableComponents);
var handler = OnlinePlayDependencies.RequestsHandler;
@@ -106,7 +101,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
///
/// The online play dependencies.
///
- public OnlinePlayTestSceneDependencies OnlinePlayDependencies { get; set; }
+ public OnlinePlayTestSceneDependencies? OnlinePlayDependencies { get; set; }
private readonly IReadOnlyDependencyContainer parent;
private readonly DependencyContainer injectableDependencies;
diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs
index 975423d19b..64bd27b871 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs
@@ -56,10 +56,10 @@ namespace osu.Game.Tests.Visual.OnlinePlay
CacheAs(BeatmapLookupCache);
}
- public object Get(Type type)
+ public object? Get(Type type)
=> dependencies.Get(type);
- public object Get(Type type, CacheInfo info)
+ public object? Get(Type type, CacheInfo info)
=> dependencies.Get(type, info);
public void Inject(T instance)
diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs
index 947305439e..6069fe4fb0 100644
--- a/osu.Game/Tests/Visual/OsuGameTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs
@@ -178,6 +178,7 @@ namespace osu.Game.Tests.Visual
LocalConfig.SetValue(OsuSetting.ShowFirstRunSetup, false);
API.Login("Rhythm Champion", "osu!");
+ ((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
Dependencies.Get().SetValue(Static.MutedAudioNotificationShownOnce, true);
diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs
index f371cf721f..c9acfa0ee5 100644
--- a/osu.Game/Tests/Visual/SkinnableTestScene.cs
+++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs
@@ -171,10 +171,10 @@ namespace osu.Game.Tests.Visual
public IRenderer Renderer => host.Renderer;
public AudioManager AudioManager => Audio;
- public IResourceStore Files => null;
+ public IResourceStore Files => null!;
public new IResourceStore Resources => base.Resources;
public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore);
- RealmAccess IStorageResourceProvider.RealmAccess => null;
+ RealmAccess IStorageResourceProvider.RealmAccess => null!;
#endregion
diff --git a/osu.Game/Utils/FileUtils.cs b/osu.Game/Utils/FileUtils.cs
new file mode 100644
index 0000000000..063ab178f7
--- /dev/null
+++ b/osu.Game/Utils/FileUtils.cs
@@ -0,0 +1,72 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Threading;
+
+namespace osu.Game.Utils
+{
+ public static class FileUtils
+ {
+ ///
+ /// Attempt an IO operation multiple times and only throw if none of the attempts succeed.
+ ///
+ /// The action to perform.
+ /// The provided state.
+ /// The number of attempts (250ms wait between each).
+ /// Whether to throw an exception on failure. If false, will silently fail.
+ public static bool AttemptOperation(Action action, T state, int attempts = 10, bool throwOnFailure = true)
+ {
+ while (true)
+ {
+ try
+ {
+ action(state);
+ return true;
+ }
+ catch (Exception)
+ {
+ if (attempts-- == 0)
+ {
+ if (throwOnFailure)
+ throw;
+
+ return false;
+ }
+ }
+
+ Thread.Sleep(250);
+ }
+ }
+
+ ///
+ /// Attempt an IO operation multiple times and only throw if none of the attempts succeed.
+ ///
+ /// The action to perform.
+ /// The number of attempts (250ms wait between each).
+ /// Whether to throw an exception on failure. If false, will silently fail.
+ public static bool AttemptOperation(Action action, int attempts = 10, bool throwOnFailure = true)
+ {
+ while (true)
+ {
+ try
+ {
+ action();
+ return true;
+ }
+ catch (Exception)
+ {
+ if (attempts-- == 0)
+ {
+ if (throwOnFailure)
+ throw;
+
+ return false;
+ }
+ }
+
+ Thread.Sleep(250);
+ }
+ }
+ }
+}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 1b1abe3971..a5d212bffe 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,8 +36,8 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 98e8b136e5..bd6891f448 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -23,6 +23,6 @@
iossimulator-x64
-
+
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/300076680-5cbe0121-ed68-414f-9ddc-dd993ac97e62.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/300076680-5cbe0121-ed68-414f-9ddc-dd993ac97e62.png
new file mode 100644
index 0000000000..21f5f0f3a0
Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/300076680-5cbe0121-ed68-414f-9ddc-dd993ac97e62.png differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
index af4b103867..29df54b400 100644
--- a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1 +1,14 @@
-{"images":[{"size":"20x20","idiom":"iphone","filename":"iPhoneNotification2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"iPhoneNotification3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"iPhoneSettings2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"iPhoneSettings3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"iPhoneSpotlight2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"iPhoneSpotlight3x.png","scale":"3x"},{"size":"60x60","idiom":"iphone","filename":"iPhoneApp2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"iPhoneApp3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"iPadNotification1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"iPadNotification2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"iPadSettings1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"iPadSettings2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"iPadSpotlight1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"iPadSpotlight2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"iPadApp1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"iPadApp2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"iPadProApp2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"iOSAppStore.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
\ No newline at end of file
+{
+ "images" : [
+ {
+ "filename" : "300076680-5cbe0121-ed68-414f-9ddc-dd993ac97e62.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iOSAppStore.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iOSAppStore.png
deleted file mode 100644
index 0e8bb029bc..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iOSAppStore.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp1x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp1x.png
deleted file mode 100644
index 42fead2364..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp1x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp2x.png
deleted file mode 100644
index 785db50cb2..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification1x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification1x.png
deleted file mode 100644
index 8c483a0a7a..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification1x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification2x.png
deleted file mode 100644
index a45b01b91c..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadProApp2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadProApp2x.png
deleted file mode 100644
index d2ba8f3a7e..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadProApp2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings1x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings1x.png
deleted file mode 100644
index 43d577040e..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings1x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings2x.png
deleted file mode 100644
index 1ebec1390b..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight1x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight1x.png
deleted file mode 100644
index a45b01b91c..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight1x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight2x.png
deleted file mode 100644
index 717603dd68..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp2x.png
deleted file mode 100644
index 6b61c09db5..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp3x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp3x.png
deleted file mode 100644
index 78ef8d12b7..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp3x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification2x.png
deleted file mode 100644
index a45b01b91c..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification3x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification3x.png
deleted file mode 100644
index 46ddf1179d..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification3x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings2x.png
deleted file mode 100644
index 1ebec1390b..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings3x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings3x.png
deleted file mode 100644
index a8145f0246..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings3x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight2x.png
deleted file mode 100644
index 717603dd68..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight2x.png and /dev/null differ
diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight3x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight3x.png
deleted file mode 100644
index 6b61c09db5..0000000000
Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight3x.png and /dev/null differ
diff --git a/osu.iOS/iTunesArtwork b/osu.iOS/iTunesArtwork
deleted file mode 100644
index 1939459992..0000000000
Binary files a/osu.iOS/iTunesArtwork and /dev/null differ
diff --git a/osu.iOS/iTunesArtwork@2x b/osu.iOS/iTunesArtwork@2x
deleted file mode 100644
index 0e8bb029bc..0000000000
Binary files a/osu.iOS/iTunesArtwork@2x and /dev/null differ
diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings
index c8c5d6745c..1bf8aa7b0b 100644
--- a/osu.sln.DotSettings
+++ b/osu.sln.DotSettings
@@ -66,6 +66,7 @@
HINT
WARNING
DO_NOT_SHOW
+ HINT
WARNING
WARNING
WARNING
@@ -81,6 +82,7 @@
WARNING
WARNING
HINT
+ HINT
WARNING
HINT
DO_NOT_SHOW
@@ -165,6 +167,7 @@
WARNING
WARNING
WARNING
+ HINT
WARNING
WARNING
WARNING
@@ -251,6 +254,7 @@
HINT
DO_NOT_SHOW
WARNING
+ HINT
WARNING
WARNING
WARNING
@@ -263,6 +267,7 @@
WARNING
WARNING
WARNING
+ HINT
WARNING
HINT
HINT