diff --git a/.github/workflows/sentry-release.yml b/.github/workflows/sentry-release.yml
index 8ca9f38234..442b97c473 100644
--- a/.github/workflows/sentry-release.yml
+++ b/.github/workflows/sentry-release.yml
@@ -23,4 +23,4 @@ jobs:
SENTRY_URL: https://sentry.ppy.sh/
with:
environment: production
- version: ${{ github.ref }}
+ version: osu@${{ github.ref_name }}
diff --git a/osu.Android.props b/osu.Android.props
index aaea784852..aad8cf10d0 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,11 +51,11 @@
-
-
+
+
-
+
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
index 5dd7c23ab6..746bdae02e 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Database;
+using osu.Game.Overlays;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Screens.Edit;
@@ -45,6 +46,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
{
(typeof(EditorBeatmap), editorBeatmap),
(typeof(IBeatSnapProvider), editorBeatmap),
+ (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)),
},
Child = new ComposeScreen { State = { Value = Visibility.Visible } },
};
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
index 715614a201..a5bd126782 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs
@@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Mania.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
- [TestCase(2.3449735700206298d, 151, "diffcalc-test")]
+ [TestCase(2.3449735700206298d, 242, "diffcalc-test")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(2.7879104989252959d, 151, "diffcalc-test")]
+ [TestCase(2.7879104989252959d, 242, "diffcalc-test")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
index 5b7a460079..c35a3dcdc2 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
foreach (var v in base.ToDatabaseAttributes())
yield return v;
- // Todo: osu!mania doesn't output MaxCombo attribute for some reason.
+ yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
yield return (ATTRIB_ID_SCORE_MULTIPLIER, ScoreMultiplier);
@@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
base.FromDatabaseAttributes(values);
+ MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
StarRating = values[ATTRIB_ID_DIFFICULTY];
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
ScoreMultiplier = values[ATTRIB_ID_SCORE_MULTIPLIER];
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
index b17aa7fc4d..88f51bf961 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
@@ -52,10 +52,18 @@ namespace osu.Game.Rulesets.Mania.Difficulty
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
ScoreMultiplier = getScoreMultiplier(mods),
- MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
+ MaxCombo = beatmap.HitObjects.Sum(maxComboForObject)
};
}
+ private static int maxComboForObject(HitObject hitObject)
+ {
+ if (hitObject is HoldNote hold)
+ return 1 + (int)((hold.EndTime - hold.StartTime) / 100);
+
+ return 1;
+ }
+
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
var sortedObjects = beatmap.HitObjects.ToArray();
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index a36f07ff7b..496d495b43 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -17,11 +17,11 @@ using osu.Framework.Testing.Input;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration;
-using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
+using osu.Game.Tests.Gameplay;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public TestSceneGameplayCursor()
{
var ruleset = new OsuRuleset();
- gameplayState = new GameplayState(CreateBeatmap(ruleset.RulesetInfo), ruleset, Array.Empty());
+ gameplayState = TestGameplayState.Create(ruleset);
AddStep("change background colour", () =>
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
index 23500f5da6..79ff222a89 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods
if (positionInfo == positionInfos.First())
{
- positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2);
+ positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2);
positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
}
else
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
index 1e170036e4..a58f62736b 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
@@ -78,7 +78,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}
});
- if (!(source.FindProvider(s => s.GetTexture("spinner-top") != null) is DefaultLegacySkin))
+ var topProvider = source.FindProvider(s => s.GetTexture("spinner-top") != null);
+
+ if (topProvider is LegacySkinTransformer transformer && !(transformer.Skin is DefaultLegacySkin))
{
AddInternal(ApproachCircle = new Sprite
{
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
index da73c2addb..266f7d1251 100644
--- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
@@ -116,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Utils
if (!(osuObject is Slider slider))
return;
+ // No need to update the head and tail circles, since slider handles that when the new slider path is set
slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
@@ -137,6 +138,7 @@ namespace osu.Game.Rulesets.Osu.Utils
if (!(osuObject is Slider slider))
return;
+ // No need to update the head and tail circles, since slider handles that when the new slider path is set
slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
@@ -146,5 +148,41 @@ namespace osu.Game.Rulesets.Osu.Utils
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
}
+
+ ///
+ /// Rotate a slider about its start position by the specified angle.
+ ///
+ /// The slider to be rotated.
+ /// The angle, measured in radians, to rotate the slider by.
+ public static void RotateSlider(Slider slider, float rotation)
+ {
+ void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position;
+
+ // No need to update the head and tail circles, since slider handles that when the new slider path is set
+ slider.NestedHitObjects.OfType().ForEach(rotateNestedObject);
+ slider.NestedHitObjects.OfType().ForEach(rotateNestedObject);
+
+ var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
+ foreach (var point in controlPoints)
+ point.Position = rotateVector(point.Position, rotation);
+
+ slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
+ }
+
+ ///
+ /// Rotate a vector by the specified angle.
+ ///
+ /// The vector to be rotated.
+ /// The angle, measured in radians, to rotate the vector by.
+ /// The rotated vector.
+ private static Vector2 rotateVector(Vector2 vector, float rotation)
+ {
+ float angle = MathF.Atan2(vector.Y, vector.X) + rotation;
+ float length = vector.Length;
+ return new Vector2(
+ length * MathF.Cos(angle),
+ length * MathF.Sin(angle)
+ );
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
index d1bc3b45df..a77d1f8b0f 100644
--- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Primitives;
+using osu.Framework.Utils;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osuTK;
@@ -37,15 +38,23 @@ namespace osu.Game.Rulesets.Osu.Utils
foreach (OsuHitObject hitObject in hitObjects)
{
Vector2 relativePosition = hitObject.Position - previousPosition;
- float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
+ float absoluteAngle = MathF.Atan2(relativePosition.Y, relativePosition.X);
float relativeAngle = absoluteAngle - previousAngle;
- positionInfos.Add(new ObjectPositionInfo(hitObject)
+ ObjectPositionInfo positionInfo;
+ positionInfos.Add(positionInfo = new ObjectPositionInfo(hitObject)
{
RelativeAngle = relativeAngle,
DistanceFromPrevious = relativePosition.Length
});
+ if (hitObject is Slider slider)
+ {
+ float absoluteRotation = getSliderRotation(slider);
+ positionInfo.Rotation = absoluteRotation - absoluteAngle;
+ absoluteAngle = absoluteRotation;
+ }
+
previousPosition = hitObject.EndPosition;
previousAngle = absoluteAngle;
}
@@ -70,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Utils
if (hitObject is Spinner)
{
- previous = null;
+ previous = current;
continue;
}
@@ -124,16 +133,23 @@ namespace osu.Game.Rulesets.Osu.Utils
if (previous != null)
{
- Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
- Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
- previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
+ if (previous.HitObject is Slider s)
+ {
+ previousAbsoluteAngle = getSliderRotation(s);
+ }
+ else
+ {
+ Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
+ Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
+ previousAbsoluteAngle = MathF.Atan2(relativePosition.Y, relativePosition.X);
+ }
}
float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle;
var posRelativeToPrev = new Vector2(
- current.PositionInfo.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
- current.PositionInfo.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
+ current.PositionInfo.DistanceFromPrevious * MathF.Cos(absoluteAngle),
+ current.PositionInfo.DistanceFromPrevious * MathF.Sin(absoluteAngle)
);
Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre;
@@ -141,6 +157,19 @@ namespace osu.Game.Rulesets.Osu.Utils
posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
current.PositionModified = lastEndPosition + posRelativeToPrev;
+
+ if (!(current.HitObject is Slider slider))
+ return;
+
+ absoluteAngle = MathF.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X);
+
+ Vector2 centreOfMassOriginal = calculateCentreOfMass(slider);
+ Vector2 centreOfMassModified = rotateVector(centreOfMassOriginal, current.PositionInfo.Rotation + absoluteAngle - getSliderRotation(slider));
+ centreOfMassModified = RotateAwayFromEdge(current.PositionModified, centreOfMassModified);
+
+ float relativeRotation = MathF.Atan2(centreOfMassModified.Y, centreOfMassModified.X) - MathF.Atan2(centreOfMassOriginal.Y, centreOfMassOriginal.X);
+ if (!Precision.AlmostEquals(relativeRotation, 0))
+ RotateSlider(slider, relativeRotation);
}
///
@@ -172,13 +201,13 @@ namespace osu.Game.Rulesets.Osu.Utils
var previousPosition = workingObject.PositionModified;
// Clamp slider position to the placement area
- // If the slider is larger than the playfield, force it to stay at the original position
+ // If the slider is larger than the playfield, at least make sure that the head circle is inside the playfield
float newX = possibleMovementBounds.Width < 0
- ? workingObject.PositionOriginal.X
+ ? Math.Clamp(possibleMovementBounds.Left, 0, OsuPlayfield.BASE_SIZE.X)
: Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
float newY = possibleMovementBounds.Height < 0
- ? workingObject.PositionOriginal.Y
+ ? Math.Clamp(possibleMovementBounds.Top, 0, OsuPlayfield.BASE_SIZE.Y)
: Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
@@ -287,6 +316,45 @@ namespace osu.Game.Rulesets.Osu.Utils
);
}
+ ///
+ /// Estimate the centre of mass of a slider relative to its start position.
+ ///
+ /// The slider to process.
+ /// The centre of mass of the slider.
+ private static Vector2 calculateCentreOfMass(Slider slider)
+ {
+ const double sample_step = 50;
+
+ // just sample the start and end positions if the slider is too short
+ if (slider.Distance <= sample_step)
+ {
+ return Vector2.Divide(slider.Path.PositionAt(1), 2);
+ }
+
+ int count = 0;
+ Vector2 sum = Vector2.Zero;
+ double pathDistance = slider.Distance;
+
+ for (double i = 0; i < pathDistance; i += sample_step)
+ {
+ sum += slider.Path.PositionAt(i / pathDistance);
+ count++;
+ }
+
+ return sum / count;
+ }
+
+ ///
+ /// Get the absolute rotation of a slider, defined as the angle from its start position to the end of its path.
+ ///
+ /// The slider to process.
+ /// The angle in radians.
+ private static float getSliderRotation(Slider slider)
+ {
+ var endPositionVector = slider.Path.PositionAt(1);
+ return MathF.Atan2(endPositionVector.Y, endPositionVector.X);
+ }
+
public class ObjectPositionInfo
{
///
@@ -309,6 +377,13 @@ namespace osu.Game.Rulesets.Osu.Utils
///
public float DistanceFromPrevious { get; set; }
+ ///
+ /// The rotation of the hit object, relative to its jump angle.
+ /// For sliders, this is defined as the angle from the slider's start position to the end of its path, relative to its jump angle.
+ /// For hit circles and spinners, this property is ignored.
+ ///
+ public float Rotation { get; set; }
+
///
/// The hit object associated with this .
///
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index 89baaf228d..e2d9910b82 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -898,5 +898,24 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(controlPoints[3].Type, Is.Null);
}
}
+
+ [Test]
+ public void TestLegacyDuplicateInitialCatmullPointIsMerged()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("catmull-duplicate-initial-controlpoint.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var decoded = decoder.Decode(stream);
+ var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints;
+
+ Assert.That(controlPoints.Count, Is.EqualTo(4));
+ Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.Catmull));
+ Assert.That(controlPoints[0].Position, Is.EqualTo(Vector2.Zero));
+ Assert.That(controlPoints[1].Type, Is.Null);
+ Assert.That(controlPoints[1].Position, Is.Not.EqualTo(Vector2.Zero));
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs
index 00276955aa..d4956e97e0 100644
--- a/osu.Game.Tests/Database/BeatmapImporterTests.cs
+++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs
@@ -710,7 +710,7 @@ namespace osu.Game.Tests.Database
var imported = await LoadOszIntoStore(importer, realm.Realm);
- realm.Realm.Write(() =>
+ await realm.Realm.WriteAsync(() =>
{
foreach (var b in imported.Beatmaps)
b.OnlineID = -1;
diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
index 9c307341bd..97be1dcfaa 100644
--- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
@@ -59,30 +59,34 @@ namespace osu.Game.Tests.Gameplay
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TestJudgement(HitResult.Great)) { Type = HitResult.Great });
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
+ Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1));
// No header shouldn't cause any change
- scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame());
+ scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame());
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
+ Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1));
// Reset with a miss instead.
- scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame
+ scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
{
Header = new FrameHeader(0, 0, 0, new Dictionary { { HitResult.Miss, 1 } }, DateTimeOffset.Now)
});
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
+ Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
// Reset with no judged hit.
- scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame
+ scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
{
Header = new FrameHeader(0, 0, 0, new Dictionary(), DateTimeOffset.Now)
});
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
Assert.That(scoreProcessor.JudgedHits, Is.Zero);
+ Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
}
private class TestJudgement : Judgement
diff --git a/osu.Game.Tests/Resources/catmull-duplicate-initial-controlpoint.osu b/osu.Game.Tests/Resources/catmull-duplicate-initial-controlpoint.osu
new file mode 100644
index 0000000000..7062229eed
--- /dev/null
+++ b/osu.Game.Tests/Resources/catmull-duplicate-initial-controlpoint.osu
@@ -0,0 +1,2 @@
+[HitObjects]
+200,304,23875,6,0,C|200:304|288:304|288:208|352:208,1,260,8|0
\ No newline at end of file
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
index 6a0950c6dd..073a228224 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
@@ -5,11 +5,13 @@ using System;
using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
+using osu.Game.Overlays;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@@ -23,7 +25,10 @@ namespace osu.Game.Tests.Visual.Editing
private BindableBeatDivisor bindableBeatDivisor;
private SliderBar tickSliderBar => beatDivisorControl.ChildrenOfType>().Single();
- private EquilateralTriangle tickMarkerHead => tickSliderBar.ChildrenOfType().Single();
+ private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType().Single();
+
+ [Cached]
+ private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[SetUp]
public void SetUp() => Schedule(() =>
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
index 30c8539d85..fa15c00cd4 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
@@ -9,6 +9,7 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Game.Overlays;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
@@ -47,6 +48,7 @@ namespace osu.Game.Tests.Visual.Editing
{
(typeof(EditorBeatmap), editorBeatmap),
(typeof(IBeatSnapProvider), editorBeatmap),
+ (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)),
},
Child = new ComposeScreen { State = { Value = Visibility.Visible } },
};
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
index 4b9be77471..393d3886e7 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
@@ -2,10 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
+using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Tests.Visual.Editing
@@ -13,6 +16,9 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture]
public class TestSceneEditorClock : EditorClockTestScene
{
+ [Cached]
+ private EditorBeatmap editorBeatmap = new EditorBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo));
+
public TestSceneEditorClock()
{
Add(new FillFlowContainer
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs
index 3cb44d9ae8..ad6fc55a32 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs
@@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
using osu.Game.Screens.Edit.Components.Menus;
namespace osu.Game.Tests.Visual.Editing
@@ -13,6 +15,9 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture]
public class TestSceneEditorMenuBar : OsuTestScene
{
+ [Cached]
+ private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
+
public TestSceneEditorMenuBar()
{
Add(new Container
diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs
index 6aa884a197..bf0a7876a9 100644
--- a/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs
+++ b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs
@@ -12,7 +12,7 @@ using osuTK;
namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
- public class TestScenePlaybackControl : OsuTestScene
+ public class TestScenePlaybackControl : EditorClockTestScene
{
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTapButton.cs b/osu.Game.Tests/Visual/Editing/TestSceneTapButton.cs
new file mode 100644
index 0000000000..d8141619ab
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTapButton.cs
@@ -0,0 +1,48 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Overlays;
+using osu.Game.Screens.Edit.Timing;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public class TestSceneTapButton : OsuManualInputManagerTestScene
+ {
+ private TapButton tapButton;
+
+ [Cached]
+ private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
+
+ [Test]
+ public void TestBasic()
+ {
+ AddStep("create button", () =>
+ {
+ Child = tapButton = new TapButton
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(4),
+ };
+ });
+
+ bool pressed = false;
+
+ AddRepeatStep("Press button", () =>
+ {
+ InputManager.MoveMouseTo(tapButton);
+ if (!pressed)
+ InputManager.PressButton(MouseButton.Left);
+ else
+ InputManager.ReleaseButton(MouseButton.Left);
+
+ pressed = !pressed;
+ }, 100);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs
index de441995b5..a1218aa3e7 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs
@@ -4,15 +4,15 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
-using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing;
using osuTK;
@@ -22,9 +22,9 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture]
public class TestSceneTapTimingControl : EditorClockTestScene
{
- [Cached(typeof(EditorBeatmap))]
- [Cached(typeof(IBeatSnapProvider))]
- private readonly EditorBeatmap editorBeatmap;
+ private EditorBeatmap editorBeatmap => editorBeatmapContainer?.EditorBeatmap;
+
+ private TestSceneHitObjectComposer.EditorBeatmapContainer editorBeatmapContainer;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
@@ -33,60 +33,48 @@ namespace osu.Game.Tests.Visual.Editing
private Bindable selectedGroup = new Bindable();
private TapTimingControl control;
+ private OsuSpriteText timingInfo;
- public TestSceneTapTimingControl()
+ [Resolved]
+ private AudioManager audio { get; set; }
+
+ [SetUpSteps]
+ public void SetUpSteps()
{
- var playableBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
-
- // Ensure time doesn't end while testing
- playableBeatmap.BeatmapInfo.Length = 1200000;
-
- editorBeatmap = new EditorBeatmap(playableBeatmap);
-
- selectedGroup.Value = editorBeatmap.ControlPointInfo.Groups.First();
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
- Beatmap.Disabled = true;
-
- Children = new Drawable[]
+ AddStep("create beatmap", () =>
{
- new Container
+ Beatmap.Value = new WaveformTestBeatmap(audio);
+ });
+
+ AddStep("Create component", () =>
+ {
+ Child = editorBeatmapContainer = new TestSceneHitObjectComposer.EditorBeatmapContainer(Beatmap.Value)
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- AutoSizeAxes = Axes.Y,
- Width = 400,
- Scale = new Vector2(1.5f),
- Child = control = new TapTimingControl(),
- }
- };
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.Y,
+ Width = 400,
+ Scale = new Vector2(1.5f),
+ Child = control = new TapTimingControl(),
+ },
+ timingInfo = new OsuSpriteText(),
+ }
+ };
+
+ selectedGroup.Value = editorBeatmap.ControlPointInfo.Groups.First();
+ });
}
- [Test]
- public void TestTapThenReset()
+ protected override void Update()
{
- AddStep("click tap button", () =>
- {
- control.ChildrenOfType()
- .Last()
- .TriggerClick();
- });
+ base.Update();
- AddUntilStep("wait for track playing", () => Clock.IsRunning);
-
- AddStep("click reset button", () =>
- {
- control.ChildrenOfType()
- .First()
- .TriggerClick();
- });
-
- AddUntilStep("wait for track stopped", () => !Clock.IsRunning);
+ if (selectedGroup.Value != null)
+ timingInfo.Text = $"offset: {selectedGroup.Value.Time:N2} bpm: {selectedGroup.Value.ControlPoints.OfType().First().BPM:N2}";
}
[Test]
@@ -99,12 +87,40 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("click tap button", () =>
{
- control.ChildrenOfType()
+ control.ChildrenOfType()
.Last()
.TriggerClick();
});
- AddSliderStep("BPM", 30, 400, 60, bpm => editorBeatmap.ControlPointInfo.TimingPoints.First().BeatLength = 60000f / bpm);
+ AddSliderStep("BPM", 30, 400, 128, bpm =>
+ {
+ if (editorBeatmap == null)
+ return;
+
+ editorBeatmap.ControlPointInfo.TimingPoints.First().BeatLength = 60000f / bpm;
+ });
+ }
+
+ [Test]
+ public void TestTapThenReset()
+ {
+ AddStep("click tap button", () =>
+ {
+ control.ChildrenOfType()
+ .Last()
+ .TriggerClick();
+ });
+
+ AddUntilStep("wait for track playing", () => Clock.IsRunning);
+
+ AddStep("click reset button", () =>
+ {
+ control.ChildrenOfType()
+ .First()
+ .TriggerClick();
+ });
+
+ AddUntilStep("wait for track stopped", () => !Clock.IsRunning);
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs
index e6fad33a51..d55852ec43 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs
@@ -4,7 +4,9 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
@@ -18,6 +20,28 @@ namespace osu.Game.Tests.Visual.Editing
{
public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(Composer);
+ [Test]
+ public void TestContextMenu()
+ {
+ TimelineHitObjectBlueprint blueprint;
+
+ AddStep("add object", () =>
+ {
+ EditorBeatmap.Clear();
+ EditorBeatmap.Add(new HitCircle { StartTime = 3000 });
+ });
+
+ AddStep("click object", () =>
+ {
+ blueprint = this.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(blueprint);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddStep("right click", () => InputManager.Click(MouseButton.Right));
+ AddAssert("context menu open", () => this.ChildrenOfType().SingleOrDefault()?.State == MenuState.Open);
+ }
+
[Test]
public void TestDisallowZeroDurationObjects()
{
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs
index 20e58c3d2a..b78512e469 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs
@@ -4,6 +4,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Game.Overlays;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@@ -14,6 +15,9 @@ namespace osu.Game.Tests.Visual.Editing
{
public override Drawable CreateTestComponent() => Empty(); // tick display is implicitly inside the timeline.
+ [Cached]
+ private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Green);
+
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs
new file mode 100644
index 0000000000..d726bd004e
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs
@@ -0,0 +1,48 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Utils;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public class TestSceneTimelineZoom : TimelineTestScene
+ {
+ public override Drawable CreateTestComponent() => Empty();
+
+ [Test]
+ public void TestVisibleRangeUpdatesOnZoomChange()
+ {
+ double initialVisibleRange = 0;
+
+ AddStep("reset zoom", () => TimelineArea.Timeline.Zoom = 100);
+ AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange);
+
+ AddStep("scale zoom", () => TimelineArea.Timeline.Zoom = 200);
+ AddAssert("range halved", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange / 2, 1));
+ AddStep("descale zoom", () => TimelineArea.Timeline.Zoom = 50);
+ AddAssert("range doubled", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange * 2, 1));
+
+ AddStep("restore zoom", () => TimelineArea.Timeline.Zoom = 100);
+ AddAssert("range restored", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange, 1));
+ }
+
+ [Test]
+ public void TestVisibleRangeConstantOnSizeChange()
+ {
+ double initialVisibleRange = 0;
+
+ AddStep("reset timeline size", () => TimelineArea.Timeline.Width = 1);
+ AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange);
+
+ AddStep("scale timeline size", () => TimelineArea.Timeline.Width = 2);
+ AddAssert("same range", () => TimelineArea.Timeline.VisibleRange == initialVisibleRange);
+ AddStep("descale timeline size", () => TimelineArea.Timeline.Width = 0.5f);
+ AddAssert("same range", () => TimelineArea.Timeline.VisibleRange == initialVisibleRange);
+
+ AddStep("restore timeline size", () => TimelineArea.Timeline.Width = 1);
+ AddAssert("same range", () => TimelineArea.Timeline.VisibleRange == initialVisibleRange);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
index 17b8189fc7..a358166477 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
@@ -1,14 +1,18 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing;
+using osu.Game.Screens.Edit.Timing.RowAttributes;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
@@ -22,6 +26,8 @@ namespace osu.Game.Tests.Visual.Editing
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
+ private TimingScreen timingScreen;
+
protected override bool ScrollUsingMouseWheel => false;
public TestSceneTimingScreen()
@@ -36,12 +42,54 @@ namespace osu.Game.Tests.Visual.Editing
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
Beatmap.Disabled = true;
- Child = new TimingScreen
+ Child = timingScreen = new TimingScreen
{
State = { Value = Visibility.Visible },
};
}
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("Stop clock", () => Clock.Stop());
+
+ AddUntilStep("wait for rows to load", () => Child.ChildrenOfType().Any());
+ }
+
+ [Test]
+ public void TestTrackingCurrentTimeWhileRunning()
+ {
+ AddStep("Select first effect point", () =>
+ {
+ InputManager.MoveMouseTo(Child.ChildrenOfType().First());
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670);
+ AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670);
+
+ AddStep("Seek to just before next point", () => Clock.Seek(69000));
+ AddStep("Start clock", () => Clock.Start());
+
+ AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670);
+ }
+
+ [Test]
+ public void TestTrackingCurrentTimeWhilePaused()
+ {
+ AddStep("Select first effect point", () =>
+ {
+ InputManager.MoveMouseTo(Child.ChildrenOfType().First());
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670);
+ AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670);
+
+ AddStep("Seek to later", () => Clock.Seek(80000));
+ AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670);
+ }
+
protected override void Dispose(bool isDisposing)
{
Beatmap.Disabled = false;
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
index 95d11d6909..2d056bafdd 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
@@ -44,7 +44,12 @@ namespace osu.Game.Tests.Visual.Editing
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(30)
},
- scrollContainer = new ZoomableScrollContainer { RelativeSizeAxes = Axes.Both }
+ scrollContainer = new ZoomableScrollContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ }
}
},
new MenuCursor()
@@ -62,7 +67,15 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestWidthInitialization()
{
- AddAssert("Inner container width was initialized", () => innerBox.DrawWidth > 0);
+ AddAssert("Inner container width was initialized", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
+ }
+
+ [Test]
+ public void TestWidthUpdatesOnDrawSizeChanges()
+ {
+ AddStep("Shrink scroll container", () => scrollContainer.Width = 0.5f);
+ AddAssert("Scroll container width shrunk", () => scrollContainer.DrawWidth == scrollContainer.Parent.DrawWidth / 2);
+ AddAssert("Inner container width matches scroll container", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
index 4aed445d9d..93bfb288d2 100644
--- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
+++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
+using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit;
@@ -38,25 +39,29 @@ namespace osu.Game.Tests.Visual.Editing
Composer = playable.BeatmapInfo.Ruleset.CreateInstance().CreateHitObjectComposer().With(d => d.Alpha = 0);
- AddRange(new Drawable[]
+ Add(new OsuContextMenuContainer
{
- EditorBeatmap,
- Composer,
- new FillFlowContainer
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
{
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 5),
- Children = new Drawable[]
+ EditorBeatmap,
+ Composer,
+ new FillFlowContainer
{
- new StartStopButton(),
- new AudioVisualiser(),
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 5),
+ Children = new Drawable[]
+ {
+ new StartStopButton(),
+ new AudioVisualiser(),
+ }
+ },
+ TimelineArea = new TimelineArea(CreateTestComponent())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
}
- },
- TimelineArea = new TimelineArea(CreateTestComponent())
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
}
});
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
index 53364b6d89..e9aa85f4ce 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
@@ -6,7 +6,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Lists;
using osu.Framework.Testing;
using osu.Framework.Timing;
@@ -22,7 +21,6 @@ using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osu.Game.Storyboards;
-using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -33,18 +31,6 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved]
private SkinManager skinManager { get; set; }
- [Cached]
- private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
-
- [Cached(typeof(HealthProcessor))]
- private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
-
- [Cached]
- private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
-
- [Cached]
- private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
-
protected override bool HasCustomSteps => true;
[Test]
@@ -81,11 +67,19 @@ namespace osu.Game.Tests.Visual.Gameplay
if (expectedComponentsContainer == null)
return false;
- var expectedComponentsAdjustmentContainer = new Container
+ var expectedComponentsAdjustmentContainer = new DependencyProvidingContainer
{
Position = actualComponentsContainer.Parent.ToSpaceOfOtherDrawable(actualComponentsContainer.DrawPosition, Content),
Size = actualComponentsContainer.DrawSize,
Child = expectedComponentsContainer,
+ // proxy the same required dependencies that `actualComponentsContainer` is using.
+ CachedDependencies = new (Type, object)[]
+ {
+ (typeof(ScoreProcessor), actualComponentsContainer.Dependencies.Get()),
+ (typeof(HealthProcessor), actualComponentsContainer.Dependencies.Get()),
+ (typeof(GameplayState), actualComponentsContainer.Dependencies.Get()),
+ (typeof(GameplayClock), actualComponentsContainer.Dependencies.Get())
+ },
};
Add(expectedComponentsAdjustmentContainer);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index 2d12645811..83c557ee51 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -15,7 +15,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
-using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Gameplay;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
@@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
[Cached]
- private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
+ private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
[Cached]
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
index 81763564fa..8362739d3b 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@@ -14,16 +13,15 @@ using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges;
using osu.Framework.Testing;
using osu.Framework.Threading;
-using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Replays;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
+using osu.Game.Tests.Gameplay;
using osu.Game.Tests.Mods;
using osuTK;
using osuTK.Graphics;
@@ -41,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private TestReplayRecorder recorder;
[Cached]
- private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty());
+ private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
[SetUpSteps]
public void SetUpSteps()
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs
index 8150252d45..5f838b8813 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs
@@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Skinning.Editor;
-using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Gameplay;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
[Cached]
- private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
+ private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
[Cached]
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
index ac5e408d90..5f2d9ee9e8 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
@@ -16,7 +16,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
-using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Gameplay;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
@@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
[Cached]
- private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
+ private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
[Cached]
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
index 8b420cebc8..b5cdd61ee5 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
@@ -18,8 +18,8 @@ using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play;
-using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Beatmaps.IO;
+using osu.Game.Tests.Gameplay;
using osu.Game.Tests.Visual.Multiplayer;
using osu.Game.Tests.Visual.Spectator;
using osuTK;
@@ -259,12 +259,15 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestFinalFramesPurgedBeforeEndingPlay()
{
- AddStep("begin playing", () => spectatorClient.BeginPlaying(new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()), new Score()));
+ AddStep("begin playing", () => spectatorClient.BeginPlaying(TestGameplayState.Create(new OsuRuleset()), new Score()));
AddStep("send frames and finish play", () =>
{
spectatorClient.HandleFrame(new OsuReplayFrame(1000, Vector2.Zero));
- spectatorClient.EndPlaying(new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()) { HasPassed = true });
+
+ var completedGameplayState = TestGameplayState.Create(new OsuRuleset());
+ completedGameplayState.HasPassed = true;
+ spectatorClient.EndPlaying(completedGameplayState);
});
// We can't access API because we're an "online" test.
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
index f8748922cf..2d2e05c4c9 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
@@ -20,13 +20,13 @@ using osu.Game.Online.Spectator;
using osu.Game.Replays;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
+using osu.Game.Tests.Gameplay;
using osu.Game.Tests.Mods;
using osu.Game.Tests.Visual.Spectator;
using osuTK;
@@ -65,7 +65,7 @@ namespace osu.Game.Tests.Visual.Gameplay
CachedDependencies = new[]
{
(typeof(SpectatorClient), (object)(spectatorClient = new TestSpectatorClient())),
- (typeof(GameplayState), new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty()))
+ (typeof(GameplayState), TestGameplayState.Create(new OsuRuleset()))
},
Children = new Drawable[]
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs
index 7d010592ae..3172a68b81 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs
@@ -124,13 +124,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
Status = { Value = new RoomStatusOpen() },
Category = { Value = RoomCategory.Spotlight },
}),
+ createLoungeRoom(new Room
+ {
+ Name = { Value = "Featured artist room" },
+ Status = { Value = new RoomStatusOpen() },
+ Category = { Value = RoomCategory.FeaturedArtist },
+ }),
}
};
});
- AddUntilStep("wait for panel load", () => rooms.Count == 5);
+ AddUntilStep("wait for panel load", () => rooms.Count == 6);
AddUntilStep("correct status text", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)) == 2);
- AddUntilStep("correct status text", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 3);
+ AddUntilStep("correct status text", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 4);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs
index c37bff2066..a8471edbf8 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs
@@ -9,7 +9,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Testing;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Mods;
@@ -73,19 +72,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
createFreeModSelect();
+ AddAssert("select all button enabled", () => this.ChildrenOfType().Single().Enabled.Value);
+
AddStep("click select all button", () =>
{
- InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(1));
+ InputManager.MoveMouseTo(this.ChildrenOfType().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("all mods selected", assertAllAvailableModsSelected);
+ AddAssert("select all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value);
AddStep("click deselect all button", () =>
{
- InputManager.MoveMouseTo(this.ChildrenOfType().Last());
+ InputManager.MoveMouseTo(this.ChildrenOfType().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("all mods deselected", () => !freeModSelectOverlay.SelectedMods.Value.Any());
+ AddAssert("select all button enabled", () => this.ChildrenOfType().Single().Enabled.Value);
}
private void createFreeModSelect()
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
index 859727e632..9d206af40e 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
@@ -10,7 +10,10 @@ using osu.Game.Rulesets;
using System;
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps.Drawables;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays.BeatmapSet.Scores;
using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Tests.Visual.Online
@@ -101,6 +104,14 @@ namespace osu.Game.Tests.Visual.Online
AddStep("show many difficulties", () => overlay.ShowBeatmapSet(createManyDifficultiesBeatmapSet()));
downloadAssert(true);
+
+ AddAssert("status is loved", () => overlay.ChildrenOfType().Single().Status == BeatmapOnlineStatus.Loved);
+ AddAssert("scores container is visible", () => overlay.ChildrenOfType().Single().Alpha == 1);
+
+ AddStep("go to second beatmap", () => overlay.ChildrenOfType().ElementAt(1).TriggerClick());
+
+ AddAssert("status is graveyard", () => overlay.ChildrenOfType().Single().Status == BeatmapOnlineStatus.Graveyard);
+ AddAssert("scores container is hidden", () => overlay.ChildrenOfType().Single().Alpha == 0);
}
[Test]
@@ -232,6 +243,7 @@ namespace osu.Game.Tests.Visual.Online
Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(),
},
+ Status = i % 2 == 0 ? BeatmapOnlineStatus.Graveyard : BeatmapOnlineStatus.Loved,
});
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs
index e4bc5645b6..39a4f1a8a1 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs
@@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Online
{
leaveText.Text = $"OnRequestLeave: {channel.Name}";
leaveText.FadeOutFromOne(1000, Easing.InQuint);
- selected.Value = null;
+ selected.Value = channelList.ChannelListingChannel;
channelList.RemoveChannel(channel);
};
@@ -112,6 +112,12 @@ namespace osu.Game.Tests.Visual.Online
for (int i = 0; i < 10; i++)
channelList.AddChannel(createRandomPrivateChannel());
});
+
+ AddStep("Add Announce Channels", () =>
+ {
+ for (int i = 0; i < 2; i++)
+ channelList.AddChannel(createRandomAnnounceChannel());
+ });
}
[Test]
@@ -170,5 +176,16 @@ namespace osu.Game.Tests.Visual.Online
Username = $"test user {id}",
});
}
+
+ private Channel createRandomAnnounceChannel()
+ {
+ int id = RNG.Next(0, 10000);
+ return new Channel
+ {
+ Name = $"Announce {id}",
+ Type = ChannelType.Announce,
+ Id = id,
+ };
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelTabControl.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelTabControl.cs
deleted file mode 100644
index e6eaffc4c1..0000000000
--- a/osu.Game.Tests/Visual/Online/TestSceneChannelTabControl.cs
+++ /dev/null
@@ -1,129 +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.Collections.Generic;
-using System.Linq;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Extensions.IEnumerableExtensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Utils;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Online.Chat;
-using osu.Game.Overlays.Chat.Tabs;
-using osuTK.Graphics;
-
-namespace osu.Game.Tests.Visual.Online
-{
- public class TestSceneChannelTabControl : OsuTestScene
- {
- private readonly TestTabControl channelTabControl;
-
- public TestSceneChannelTabControl()
- {
- SpriteText currentText;
- Add(new Container
- {
- RelativeSizeAxes = Axes.X,
- Origin = Anchor.Centre,
- Anchor = Anchor.Centre,
- Children = new Drawable[]
- {
- channelTabControl = new TestTabControl
- {
- RelativeSizeAxes = Axes.X,
- Origin = Anchor.Centre,
- Anchor = Anchor.Centre,
- Height = 50
- },
- new Box
- {
- Colour = Color4.Black.Opacity(0.1f),
- RelativeSizeAxes = Axes.X,
- Height = 50,
- Depth = -1,
- Origin = Anchor.Centre,
- Anchor = Anchor.Centre,
- }
- }
- });
-
- Add(new Container
- {
- Origin = Anchor.TopLeft,
- Anchor = Anchor.TopLeft,
- Children = new Drawable[]
- {
- currentText = new OsuSpriteText
- {
- Text = "Currently selected channel:"
- }
- }
- });
-
- channelTabControl.OnRequestLeave += channel => channelTabControl.RemoveChannel(channel);
- channelTabControl.Current.ValueChanged += channel => currentText.Text = "Currently selected channel: " + channel.NewValue;
-
- AddStep("Add random private channel", addRandomPrivateChannel);
- AddAssert("There is only one channels", () => channelTabControl.Items.Count == 2);
- AddRepeatStep("Add 3 random private channels", addRandomPrivateChannel, 3);
- AddAssert("There are four channels", () => channelTabControl.Items.Count == 5);
- AddStep("Add random public channel", () => addChannel(RNG.Next().ToString()));
-
- AddRepeatStep("Select a random channel", () =>
- {
- List validChannels = channelTabControl.Items.Where(c => !(c is ChannelSelectorTabItem.ChannelSelectorTabChannel)).ToList();
- channelTabControl.SelectChannel(validChannels[RNG.Next(0, validChannels.Count)]);
- }, 20);
-
- Channel channelBefore = null;
- AddStep("set first channel", () => channelTabControl.SelectChannel(channelBefore = channelTabControl.Items.First(c => !(c is ChannelSelectorTabItem.ChannelSelectorTabChannel))));
-
- AddStep("select selector tab", () => channelTabControl.SelectChannel(channelTabControl.Items.Single(c => c is ChannelSelectorTabItem.ChannelSelectorTabChannel)));
- AddAssert("selector tab is active", () => channelTabControl.ChannelSelectorActive.Value);
-
- AddAssert("check channel unchanged", () => channelBefore == channelTabControl.Current.Value);
-
- AddStep("set second channel", () => channelTabControl.SelectChannel(channelTabControl.Items.GetNext(channelBefore)));
- AddAssert("selector tab is inactive", () => !channelTabControl.ChannelSelectorActive.Value);
-
- AddUntilStep("remove all channels", () =>
- {
- foreach (var item in channelTabControl.Items.ToList())
- {
- if (item is ChannelSelectorTabItem.ChannelSelectorTabChannel)
- continue;
-
- channelTabControl.RemoveChannel(item);
- return false;
- }
-
- return true;
- });
-
- AddAssert("selector tab is active", () => channelTabControl.ChannelSelectorActive.Value);
- }
-
- private void addRandomPrivateChannel() =>
- channelTabControl.AddChannel(new Channel(new APIUser
- {
- Id = RNG.Next(1000, 10000000),
- Username = "Test User " + RNG.Next(1000)
- }));
-
- private void addChannel(string name) =>
- channelTabControl.AddChannel(new Channel
- {
- Type = ChannelType.Public,
- Name = name
- });
-
- private class TestTabControl : ChannelTabControl
- {
- public void SelectChannel(Channel channel) => base.SelectTab(TabMap[channel]);
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
index 6818147da4..a28de3be1e 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
@@ -12,7 +12,6 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
-using osu.Game.Overlays;
using osu.Game.Overlays.Chat;
using osuTK.Graphics;
@@ -22,12 +21,10 @@ namespace osu.Game.Tests.Visual.Online
public class TestSceneChatLink : OsuTestScene
{
private readonly TestChatLineContainer textContainer;
- private readonly DialogOverlay dialogOverlay;
private Color4 linkColour;
public TestSceneChatLink()
{
- Add(dialogOverlay = new DialogOverlay { Depth = float.MinValue });
Add(textContainer = new TestChatLineContainer
{
Padding = new MarginPadding { Left = 20, Right = 20 },
@@ -47,9 +44,6 @@ namespace osu.Game.Tests.Visual.Online
availableChannels.Add(new Channel { Name = "#english" });
availableChannels.Add(new Channel { Name = "#japanese" });
Dependencies.Cache(chatManager);
-
- Dependencies.Cache(new ChatOverlay());
- Dependencies.CacheAs(dialogOverlay);
}
[SetUp]
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
index 4d1dee1650..2cf1114f30 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
@@ -1,19 +1,22 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using System.Linq;
+using System.Collections.Generic;
using System.Net;
+using System.Threading;
using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
+using osu.Framework.Logging;
using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
@@ -21,387 +24,223 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
using osu.Game.Overlays.Chat;
-using osu.Game.Overlays.Chat.Selection;
-using osu.Game.Overlays.Chat.Tabs;
+using osu.Game.Overlays.Chat.Listing;
+using osu.Game.Overlays.Chat.ChannelList;
+using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Online
{
+ [TestFixture]
public class TestSceneChatOverlay : OsuManualInputManagerTestScene
{
private TestChatOverlay chatOverlay;
private ChannelManager channelManager;
- private IEnumerable visibleChannels => chatOverlay.ChannelTabControl.VisibleItems.Where(channel => channel.Name != "+");
- private IEnumerable joinedChannels => chatOverlay.ChannelTabControl.Items.Where(channel => channel.Name != "+");
- private readonly List channels;
+ private APIUser testUser;
+ private Channel testPMChannel;
+ private Channel[] testChannels;
- private Channel currentChannel => channelManager.CurrentChannel.Value;
- private Channel nextChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) + 1);
- private Channel previousChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) - 1);
- private Channel channel1 => channels[0];
- private Channel channel2 => channels[1];
- private Channel channel3 => channels[2];
+ private Channel testChannel1 => testChannels[0];
+ private Channel testChannel2 => testChannels[1];
- [CanBeNull]
- private Func> onGetMessages;
-
- public TestSceneChatOverlay()
- {
- channels = Enumerable.Range(1, 10)
- .Select(index => new Channel(new APIUser())
- {
- Name = $"Channel no. {index}",
- Topic = index == 3 ? null : $"We talk about the number {index} here",
- Type = index % 2 == 0 ? ChannelType.PM : ChannelType.Temporary,
- Id = index
- })
- .ToList();
- }
+ [Resolved]
+ private OsuConfigManager config { get; set; } = null!;
[SetUp]
- public void Setup()
+ public void SetUp() => Schedule(() =>
{
- Schedule(() =>
+ testUser = new APIUser { Username = "test user", Id = 5071479 };
+ testPMChannel = new Channel(testUser);
+ testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray();
+
+ Child = new DependencyProvidingContainer
{
- ChannelManagerContainer container;
-
- Child = container = new ChannelManagerContainer(channels)
+ RelativeSizeAxes = Axes.Both,
+ CachedDependencies = new (Type, object)[]
{
- RelativeSizeAxes = Axes.Both,
- };
-
- chatOverlay = container.ChatOverlay;
- channelManager = container.ChannelManager;
- });
- }
+ (typeof(ChannelManager), channelManager = new ChannelManager()),
+ },
+ Children = new Drawable[]
+ {
+ channelManager,
+ chatOverlay = new TestChatOverlay(),
+ },
+ };
+ });
[SetUpSteps]
public void SetUpSteps()
{
- AddStep("register request handling", () =>
+ AddStep("Setup request handler", () =>
{
- onGetMessages = null;
-
((DummyAPIAccess)API).HandleRequest = req =>
{
switch (req)
{
+ case GetUpdatesRequest getUpdates:
+ getUpdates.TriggerFailure(new WebException());
+ return true;
+
case JoinChannelRequest joinChannel:
joinChannel.TriggerSuccess();
return true;
- case GetUserRequest getUser:
- if (getUser.Lookup.Equals("some body", StringComparison.OrdinalIgnoreCase))
- {
- getUser.TriggerSuccess(new APIUser
- {
- Username = "some body",
- Id = 1,
- });
- }
- else
- {
- getUser.TriggerFailure(new WebException());
- }
-
+ case LeaveChannelRequest leaveChannel:
+ leaveChannel.TriggerSuccess();
return true;
case GetMessagesRequest getMessages:
- var messages = onGetMessages?.Invoke(getMessages.Channel);
- if (messages != null)
- getMessages.TriggerSuccess(messages);
+ getMessages.TriggerSuccess(createChannelMessages(getMessages.Channel));
return true;
- }
- return false;
+ case GetUserRequest getUser:
+ if (getUser.Lookup == testUser.Username)
+ getUser.TriggerSuccess(testUser);
+ else
+ getUser.TriggerFailure(new WebException());
+ return true;
+
+ case PostMessageRequest postMessage:
+ postMessage.TriggerSuccess(new Message(RNG.Next(0, 10000000))
+ {
+ Content = postMessage.Message.Content,
+ ChannelId = postMessage.Message.ChannelId,
+ Sender = postMessage.Message.Sender,
+ Timestamp = new DateTimeOffset(DateTime.Now),
+ });
+ return true;
+
+ default:
+ Logger.Log($"Unhandled Request Type: {req.GetType()}");
+ return false;
+ }
};
});
+
+ AddStep("Add test channels", () =>
+ {
+ (channelManager.AvailableChannels as BindableList)?.AddRange(testChannels);
+ });
}
[Test]
- public void TestHideOverlay()
+ public void TestBasic()
{
- AddStep("Open chat overlay", () => chatOverlay.Show());
+ AddStep("Show overlay with channel", () =>
+ {
+ chatOverlay.Show();
+ Channel joinedChannel = channelManager.JoinChannel(testChannel1);
+ channelManager.CurrentChannel.Value = joinedChannel;
+ });
+ AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
+ waitForChannel1Visible();
+ }
- AddAssert("Chat overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
- AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible);
+ [Test]
+ public void TestShowHide()
+ {
+ AddStep("Show overlay", () => chatOverlay.Show());
+ AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
+ AddStep("Hide overlay", () => chatOverlay.Hide());
+ AddAssert("Overlay is hidden", () => chatOverlay.State.Value == Visibility.Hidden);
+ }
- AddStep("Close chat overlay", () => chatOverlay.Hide());
+ [Test]
+ public void TestChatHeight()
+ {
+ BindableFloat configChatHeight = new BindableFloat();
- AddAssert("Chat overlay was hidden", () => chatOverlay.State.Value == Visibility.Hidden);
- AddAssert("Channel selection overlay was hidden", () => chatOverlay.SelectionOverlayState == Visibility.Hidden);
+ float newHeight = 0;
+
+ AddStep("Reset config chat height", () =>
+ {
+ config.BindWith(OsuSetting.ChatDisplayHeight, configChatHeight);
+ configChatHeight.SetDefault();
+ });
+ AddStep("Show overlay", () => chatOverlay.Show());
+ AddAssert("Overlay uses config height", () => chatOverlay.Height == configChatHeight.Default);
+ AddStep("Click top bar", () =>
+ {
+ InputManager.MoveMouseTo(chatOverlayTopBar);
+ InputManager.PressButton(MouseButton.Left);
+ });
+ AddStep("Drag overlay to new height", () => InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300)));
+ AddStep("Stop dragging", () => InputManager.ReleaseButton(MouseButton.Left));
+ AddStep("Store new height", () => newHeight = chatOverlay.Height);
+ AddAssert("Config height changed", () => !configChatHeight.IsDefault && configChatHeight.Value == newHeight);
+ AddStep("Hide overlay", () => chatOverlay.Hide());
+ AddStep("Show overlay", () => chatOverlay.Show());
+ AddAssert("Overlay uses new height", () => chatOverlay.Height == newHeight);
}
[Test]
public void TestChannelSelection()
{
- AddStep("Open chat overlay", () => chatOverlay.Show());
- AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible);
- AddStep("Setup get message response", () => onGetMessages = channel =>
- {
- if (channel == channel1)
- {
- return new List
- {
- new Message(1)
- {
- ChannelId = channel1.Id,
- Content = "hello from channel 1!",
- Sender = new APIUser
- {
- Id = 2,
- Username = "test_user"
- }
- }
- };
- }
-
- return null;
- });
-
- AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
- AddStep("Switch to channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
-
- AddAssert("Current channel is channel 1", () => currentChannel == channel1);
- AddUntilStep("Loading spinner hidden", () => chatOverlay.ChildrenOfType().All(spinner => !spinner.IsPresent));
- AddAssert("Channel message shown", () => chatOverlay.ChildrenOfType().Count() == 1);
- AddAssert("Channel selector was closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden);
+ AddStep("Show overlay", () => chatOverlay.Show());
+ AddAssert("Listing is visible", () => listingIsVisible);
+ AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
+ AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
+ waitForChannel1Visible();
}
[Test]
- public void TestSearchInSelector()
+ public void TestSearchInListing()
{
- AddStep("Open chat overlay", () => chatOverlay.Show());
- AddStep("Search for 'no. 2'", () => chatOverlay.ChildrenOfType().First().Text = "no. 2");
+ AddStep("Show overlay", () => chatOverlay.Show());
+ AddAssert("Listing is visible", () => listingIsVisible);
+ AddStep("Search for 'number 2'", () => chatOverlayTextBox.Text = "number 2");
AddUntilStep("Only channel 2 visible", () =>
{
- var listItems = chatOverlay.ChildrenOfType().Where(c => c.IsPresent);
- return listItems.Count() == 1 && listItems.Single().Channel == channel2;
+ IEnumerable listingItems = chatOverlay.ChildrenOfType()
+ .Where(item => item.IsPresent);
+ return listingItems.Count() == 1 && listingItems.Single().Channel == testChannel2;
});
}
- [Test]
- public void TestChannelShortcutKeys()
- {
- AddStep("Open chat overlay", () => chatOverlay.Show());
- AddStep("Join channels", () => channels.ForEach(channel => channelManager.JoinChannel(channel)));
- AddStep("Close channel selector", () => InputManager.Key(Key.Escape));
- AddUntilStep("Wait for close", () => chatOverlay.SelectionOverlayState == Visibility.Hidden);
-
- for (int zeroBasedIndex = 0; zeroBasedIndex < 10; ++zeroBasedIndex)
- {
- int oneBasedIndex = zeroBasedIndex + 1;
- int targetNumberKey = oneBasedIndex % 10;
- var targetChannel = channels[zeroBasedIndex];
- AddStep($"Press Alt+{targetNumberKey}", () => pressChannelHotkey(targetNumberKey));
- AddAssert($"Channel #{oneBasedIndex} is selected", () => currentChannel == targetChannel);
- }
- }
-
- private Channel expectedChannel;
-
- [Test]
- public void TestCloseChannelBehaviour()
- {
- AddStep("Open chat overlay", () => chatOverlay.Show());
- AddUntilStep("Join until dropdown has channels", () =>
- {
- if (visibleChannels.Count() < joinedChannels.Count())
- return true;
-
- // Using temporary channels because they don't hide their names when not active
- channelManager.JoinChannel(new Channel
- {
- Name = $"Channel no. {joinedChannels.Count() + 11}",
- Type = ChannelType.Temporary
- });
-
- return false;
- });
-
- AddStep("Switch to last tab", () => clickDrawable(chatOverlay.TabMap[visibleChannels.Last()]));
- AddAssert("Last visible selected", () => currentChannel == visibleChannels.Last());
-
- // Closing the last channel before dropdown
- AddStep("Close current channel", () =>
- {
- expectedChannel = nextChannel;
- chatOverlay.ChannelTabControl.RemoveChannel(currentChannel);
- });
- AddAssert("Next channel selected", () => currentChannel == expectedChannel);
- AddAssert("Selector remained closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden);
-
- // Depending on the window size, one more channel might need to be closed for the selectorTab to appear
- AddUntilStep("Close channels until selector visible", () =>
- {
- if (chatOverlay.ChannelTabControl.VisibleItems.Last().Name == "+")
- return true;
-
- chatOverlay.ChannelTabControl.RemoveChannel(visibleChannels.Last());
- return false;
- });
- AddAssert("Last visible selected", () => currentChannel == visibleChannels.Last());
-
- // Closing the last channel with dropdown no longer present
- AddStep("Close last when selector next", () =>
- {
- expectedChannel = previousChannel;
- chatOverlay.ChannelTabControl.RemoveChannel(currentChannel);
- });
- AddAssert("Previous channel selected", () => currentChannel == expectedChannel);
-
- // Standard channel closing
- AddStep("Switch to previous channel", () => chatOverlay.ChannelTabControl.SwitchTab(-1));
- AddStep("Close current channel", () =>
- {
- expectedChannel = nextChannel;
- chatOverlay.ChannelTabControl.RemoveChannel(currentChannel);
- });
- AddAssert("Next channel selected", () => currentChannel == expectedChannel);
-
- // Selector reappearing after all channels closed
- AddUntilStep("Close all channels", () =>
- {
- if (!joinedChannels.Any())
- return true;
-
- chatOverlay.ChannelTabControl.RemoveChannel(joinedChannels.Last());
- return false;
- });
- AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible);
- }
-
[Test]
public void TestChannelCloseButton()
{
- AddStep("Open chat overlay", () => chatOverlay.Show());
- AddStep("Join 2 channels", () =>
+ AddStep("Show overlay", () => chatOverlay.Show());
+ AddStep("Join PM and public channels", () =>
{
- channelManager.JoinChannel(channel1);
- channelManager.JoinChannel(channel2);
+ channelManager.JoinChannel(testChannel1);
+ channelManager.JoinChannel(testPMChannel);
});
-
- // PM channel close button only appears when active
- AddStep("Select PM channel", () => clickDrawable(chatOverlay.TabMap[channel2]));
- AddStep("Click PM close button", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child));
- AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(channel2));
-
- // Non-PM chat channel close button only appears when hovered
- AddStep("Hover normal channel tab", () => InputManager.MoveMouseTo(chatOverlay.TabMap[channel1]));
- AddStep("Click normal close button", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child));
- AddAssert("All channels closed", () => !channelManager.JoinedChannels.Any());
- }
-
- [Test]
- public void TestCloseTabShortcut()
- {
- AddStep("Open chat overlay", () => chatOverlay.Show());
- AddStep("Join 2 channels", () =>
+ AddStep("Select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
+ AddStep("Click close button", () =>
{
- channelManager.JoinChannel(channel1);
- channelManager.JoinChannel(channel2);
+ ChannelListItemCloseButton closeButton = getChannelListItem(testPMChannel).ChildrenOfType().Single();
+ clickDrawable(closeButton);
});
-
- // Want to close channel 2
- AddStep("Select channel 2", () => clickDrawable(chatOverlay.TabMap[channel2]));
- AddStep("Close tab via shortcut", pressCloseDocumentKeys);
-
- // Channel 2 should be closed
- AddAssert("Channel 1 open", () => channelManager.JoinedChannels.Contains(channel1));
- AddAssert("Channel 2 closed", () => !channelManager.JoinedChannels.Contains(channel2));
-
- // Want to close channel 1
- AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
-
- AddStep("Close tab via shortcut", pressCloseDocumentKeys);
- // Channel 1 and channel 2 should be closed
- AddAssert("All channels closed", () => !channelManager.JoinedChannels.Any());
- }
-
- [Test]
- public void TestNewTabShortcut()
- {
- AddStep("Open chat overlay", () => chatOverlay.Show());
- AddStep("Join 2 channels", () =>
+ AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(testPMChannel));
+ AddStep("Select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
+ AddStep("Click close button", () =>
{
- channelManager.JoinChannel(channel1);
- channelManager.JoinChannel(channel2);
+ ChannelListItemCloseButton closeButton = getChannelListItem(testChannel1).ChildrenOfType().Single();
+ clickDrawable(closeButton);
});
-
- // Want to join another channel
- AddStep("Press new tab shortcut", pressNewTabKeys);
-
- // Selector should be visible
- AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible);
- }
-
- [Test]
- public void TestRestoreTabShortcut()
- {
- AddStep("Open chat overlay", () => chatOverlay.Show());
- AddStep("Join 3 channels", () =>
- {
- channelManager.JoinChannel(channel1);
- channelManager.JoinChannel(channel2);
- channelManager.JoinChannel(channel3);
- });
-
- // Should do nothing
- AddStep("Restore tab via shortcut", pressRestoreTabKeys);
- AddAssert("All channels still open", () => channelManager.JoinedChannels.Count == 3);
-
- // Close channel 1
- AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
- AddStep("Click normal close button", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child));
- AddAssert("Channel 1 closed", () => !channelManager.JoinedChannels.Contains(channel1));
- AddAssert("Other channels still open", () => channelManager.JoinedChannels.Count == 2);
-
- // Reopen channel 1
- AddStep("Restore tab via shortcut", pressRestoreTabKeys);
- AddAssert("All channels now open", () => channelManager.JoinedChannels.Count == 3);
- AddAssert("Current channel is channel 1", () => currentChannel == channel1);
-
- // Close two channels
- AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
- AddStep("Close channel 1", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child));
- AddStep("Select channel 2", () => clickDrawable(chatOverlay.TabMap[channel2]));
- AddStep("Close channel 2", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child));
- AddAssert("Only one channel open", () => channelManager.JoinedChannels.Count == 1);
- AddAssert("Current channel is channel 3", () => currentChannel == channel3);
-
- // Should first re-open channel 2
- AddStep("Restore tab via shortcut", pressRestoreTabKeys);
- AddAssert("Channel 1 still closed", () => !channelManager.JoinedChannels.Contains(channel1));
- AddAssert("Channel 2 now open", () => channelManager.JoinedChannels.Contains(channel2));
- AddAssert("Current channel is channel 2", () => currentChannel == channel2);
-
- // Should then re-open channel 1
- AddStep("Restore tab via shortcut", pressRestoreTabKeys);
- AddAssert("All channels now open", () => channelManager.JoinedChannels.Count == 3);
- AddAssert("Current channel is channel 1", () => currentChannel == channel1);
+ AddAssert("Normal channel closed", () => !channelManager.JoinedChannels.Contains(testChannel1));
}
[Test]
public void TestChatCommand()
{
- AddStep("Open chat overlay", () => chatOverlay.Show());
- AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
- AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
-
- AddStep("Open chat with user", () => channelManager.PostCommand("chat some body"));
+ AddStep("Show overlay", () => chatOverlay.Show());
+ AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
+ AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
+ AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}"));
AddAssert("PM channel is selected", () =>
- channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single().Username == "some body");
-
- AddStep("Open chat with non-existent user", () => channelManager.PostCommand("chat nobody"));
+ channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser);
+ AddStep("Open chat with non-existent user", () => channelManager.PostCommand("chat user_doesnt_exist"));
AddAssert("Last message is error", () => channelManager.CurrentChannel.Value.Messages.Last() is ErrorMessage);
// Make sure no unnecessary requests are made when the PM channel is already open.
- AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
+ AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Unregister request handling", () => ((DummyAPIAccess)API).HandleRequest = null);
- AddStep("Open chat with user", () => channelManager.PostCommand("chat some body"));
+ AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}"));
AddAssert("PM channel is selected", () =>
- channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single().Username == "some body");
+ channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser);
}
[Test]
@@ -409,20 +248,17 @@ namespace osu.Game.Tests.Visual.Online
{
Channel multiplayerChannel = null;
- AddStep("open chat overlay", () => chatOverlay.Show());
-
- AddStep("join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser())
+ AddStep("Show overlay", () => chatOverlay.Show());
+ AddStep("Join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser())
{
Name = "#mp_1",
Type = ChannelType.Multiplayer,
}));
-
- AddAssert("channel joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel));
- AddAssert("channel not present in overlay", () => !chatOverlay.TabMap.ContainsKey(multiplayerChannel));
- AddAssert("multiplayer channel is not current", () => channelManager.CurrentChannel.Value != multiplayerChannel);
-
- AddStep("leave channel", () => channelManager.LeaveChannel(multiplayerChannel));
- AddAssert("channel left", () => !channelManager.JoinedChannels.Contains(multiplayerChannel));
+ AddAssert("Channel is joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel));
+ AddUntilStep("Channel not present in listing", () => !chatOverlay.ChildrenOfType()
+ .Where(item => item.IsPresent)
+ .Select(item => item.Channel)
+ .Contains(multiplayerChannel));
}
[Test]
@@ -430,26 +266,21 @@ namespace osu.Game.Tests.Visual.Online
{
Message message = null;
- AddStep("Open chat overlay", () => chatOverlay.Show());
- AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
- AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
-
+ AddStep("Show overlay", () => chatOverlay.Show());
+ AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
+ AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Send message in channel 1", () =>
{
- channel1.AddNewMessages(message = new Message
+ testChannel1.AddNewMessages(message = new Message
{
- ChannelId = channel1.Id,
+ ChannelId = testChannel1.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
- Sender = new APIUser
- {
- Id = 2,
- Username = "Someone",
- }
+ Sender = testUser,
});
});
-
- AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel1));
+ AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
+ waitForChannel1Visible();
}
[Test]
@@ -457,28 +288,22 @@ namespace osu.Game.Tests.Visual.Online
{
Message message = null;
- AddStep("Open chat overlay", () => chatOverlay.Show());
- AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
- AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
-
- AddStep("Join channel 2", () => channelManager.JoinChannel(channel2));
+ AddStep("Show overlay", () => chatOverlay.Show());
+ AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
+ AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
+ AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Send message in channel 2", () =>
{
- channel2.AddNewMessages(message = new Message
+ testChannel2.AddNewMessages(message = new Message
{
- ChannelId = channel2.Id,
+ ChannelId = testChannel2.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
- Sender = new APIUser
- {
- Id = 2,
- Username = "Someone",
- }
+ Sender = testUser,
});
});
-
- AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel2));
- AddAssert("Switched to channel 2", () => channelManager.CurrentChannel.Value == channel2);
+ AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2));
+ waitForChannel2Visible();
}
[Test]
@@ -486,30 +311,23 @@ namespace osu.Game.Tests.Visual.Online
{
Message message = null;
- AddStep("Open chat overlay", () => chatOverlay.Show());
-
- AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
- AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
-
- AddStep("Join channel 2", () => channelManager.JoinChannel(channel2));
+ AddStep("Show overlay", () => chatOverlay.Show());
+ AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
+ AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
+ AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Send message in channel 2", () =>
{
- channel2.AddNewMessages(message = new Message
+ testChannel2.AddNewMessages(message = new Message
{
- ChannelId = channel2.Id,
+ ChannelId = testChannel2.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
- Sender = new APIUser
- {
- Id = 2,
- Username = "Someone",
- }
+ Sender = testUser,
});
});
- AddStep("Leave channel 2", () => channelManager.LeaveChannel(channel2));
-
- AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel2));
- AddAssert("Switched to channel 2", () => channelManager.CurrentChannel.Value == channel2);
+ AddStep("Leave channel 2", () => channelManager.LeaveChannel(testChannel2));
+ AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2));
+ waitForChannel2Visible();
}
[Test]
@@ -517,24 +335,19 @@ namespace osu.Game.Tests.Visual.Online
{
Message message = null;
- AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
-
+ AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Send message in channel 1", () =>
{
- channel1.AddNewMessages(message = new Message
+ testChannel1.AddNewMessages(message = new Message
{
- ChannelId = channel1.Id,
+ ChannelId = testChannel1.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
- Sender = new APIUser
- {
- Id = 2,
- Username = "Someone",
- }
+ Sender = testUser,
});
});
-
- AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel1));
+ AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
+ waitForChannel1Visible();
}
[Test]
@@ -542,40 +355,169 @@ namespace osu.Game.Tests.Visual.Online
{
Message message = null;
- AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
-
+ AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Send message in channel 1", () =>
{
- channel1.AddNewMessages(message = new Message
+ testChannel1.AddNewMessages(message = new Message
{
- ChannelId = channel1.Id,
+ ChannelId = testChannel1.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
- Sender = new APIUser
- {
- Id = 2,
- Username = "Someone",
- }
+ Sender = testUser,
});
});
-
AddStep("Set null channel", () => channelManager.CurrentChannel.Value = null);
- AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel1));
+ AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
+ waitForChannel1Visible();
}
- private void pressChannelHotkey(int number)
+ [Test]
+ public void TestTextBoxRetainsFocus()
{
- var channelKey = Key.Number0 + number;
- InputManager.PressKey(Key.AltLeft);
- InputManager.Key(channelKey);
- InputManager.ReleaseKey(Key.AltLeft);
+ AddStep("Show overlay", () => chatOverlay.Show());
+ AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
+ AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
+ AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
+ waitForChannel1Visible();
+ AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
+ AddStep("Click drawable channel", () => clickDrawable(currentDrawableChannel));
+ AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
+ AddStep("Click selector", () => clickDrawable(channelSelectorButton));
+ AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
+ AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType().Single()));
+ AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
+ AddStep("Click channel list", () => clickDrawable(chatOverlay.ChildrenOfType().Single()));
+ AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
+ AddStep("Click top bar", () => clickDrawable(chatOverlay.ChildrenOfType().Single()));
+ AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
+ AddStep("Hide overlay", () => chatOverlay.Hide());
+ AddAssert("TextBox is not focused", () => InputManager.FocusedDrawable == null);
}
- private void pressCloseDocumentKeys() => InputManager.Keys(PlatformAction.DocumentClose);
+ [Test]
+ public void TestSlowLoadingChannel()
+ {
+ AddStep("Show overlay (slow-loading)", () =>
+ {
+ chatOverlay.Show();
+ chatOverlay.SlowLoading = true;
+ });
+ AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
+ AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
+ AddUntilStep("Channel 1 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Loading);
- private void pressNewTabKeys() => InputManager.Keys(PlatformAction.TabNew);
+ AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
+ AddStep("Select channel 2", () => clickDrawable(getChannelListItem(testChannel2)));
+ AddUntilStep("Channel 2 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel2).LoadState == LoadState.Loading);
- private void pressRestoreTabKeys() => InputManager.Keys(PlatformAction.TabRestore);
+ AddStep("Finish channel 1 load", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadEvent.Set());
+ AddUntilStep("Channel 1 ready", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Ready);
+ AddAssert("Channel 1 not displayed", () => !channelIsVisible);
+
+ AddStep("Finish channel 2 load", () => chatOverlay.GetSlowLoadingChannel(testChannel2).LoadEvent.Set());
+ AddUntilStep("Channel 2 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel2).IsLoaded);
+ waitForChannel2Visible();
+
+ AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
+ AddUntilStep("Channel 1 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel1).IsLoaded);
+ waitForChannel1Visible();
+ }
+
+ [Test]
+ public void TestKeyboardCloseAndRestoreChannel()
+ {
+ AddStep("Show overlay with channel 1", () =>
+ {
+ channelManager.JoinChannel(testChannel1);
+ chatOverlay.Show();
+ });
+ waitForChannel1Visible();
+ AddStep("Press document close keys", () => InputManager.Keys(PlatformAction.DocumentClose));
+ AddAssert("Listing is visible", () => listingIsVisible);
+
+ AddStep("Press tab restore keys", () => InputManager.Keys(PlatformAction.TabRestore));
+ waitForChannel1Visible();
+ }
+
+ [Test]
+ public void TestKeyboardNewChannel()
+ {
+ AddStep("Show overlay with channel 1", () =>
+ {
+ channelManager.JoinChannel(testChannel1);
+ chatOverlay.Show();
+ });
+ waitForChannel1Visible();
+ AddStep("Press tab new keys", () => InputManager.Keys(PlatformAction.TabNew));
+ AddAssert("Listing is visible", () => listingIsVisible);
+ }
+
+ [Test]
+ public void TestKeyboardNextChannel()
+ {
+ Channel announceChannel = createAnnounceChannel();
+ Channel pmChannel1 = createPrivateChannel();
+ Channel pmChannel2 = createPrivateChannel();
+
+ AddStep("Show overlay with channels", () =>
+ {
+ channelManager.JoinChannel(testChannel1);
+ channelManager.JoinChannel(testChannel2);
+ channelManager.JoinChannel(pmChannel1);
+ channelManager.JoinChannel(pmChannel2);
+ channelManager.JoinChannel(announceChannel);
+ chatOverlay.Show();
+ });
+
+ AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
+
+ waitForChannel1Visible();
+ AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext));
+ waitForChannel2Visible();
+
+ AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext));
+ AddUntilStep("PM Channel 1 displayed", () => channelIsVisible && currentDrawableChannel?.Channel == pmChannel1);
+
+ AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext));
+ AddUntilStep("PM Channel 2 displayed", () => channelIsVisible && currentDrawableChannel?.Channel == pmChannel2);
+
+ AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext));
+ AddUntilStep("Announce channel displayed", () => channelIsVisible && currentDrawableChannel?.Channel == announceChannel);
+
+ AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext));
+ waitForChannel1Visible();
+ }
+
+ private void waitForChannel1Visible() =>
+ AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel?.Channel == testChannel1);
+
+ private void waitForChannel2Visible() =>
+ AddUntilStep("Channel 2 is visible", () => channelIsVisible && currentDrawableChannel?.Channel == testChannel2);
+
+ private bool listingIsVisible =>
+ chatOverlay.ChildrenOfType().Single().State.Value == Visibility.Visible;
+
+ private bool loadingIsVisible =>
+ chatOverlay.ChildrenOfType().Single().State.Value == Visibility.Visible;
+
+ private bool channelIsVisible =>
+ !listingIsVisible && !loadingIsVisible;
+
+ [CanBeNull]
+ private DrawableChannel currentDrawableChannel =>
+ chatOverlay.ChildrenOfType().SingleOrDefault();
+
+ private ChannelListItem getChannelListItem(Channel channel) =>
+ chatOverlay.ChildrenOfType().Single(item => item.Channel == channel);
+
+ private ChatTextBox chatOverlayTextBox =>
+ chatOverlay.ChildrenOfType().Single();
+
+ private ChatOverlayTopBar chatOverlayTopBar =>
+ chatOverlay.ChildrenOfType().Single();
+
+ private ChannelListItem channelSelectorButton =>
+ chatOverlay.ChildrenOfType().Single(item => item.Channel is ChannelListing.ChannelListingChannel);
private void clickDrawable(Drawable d)
{
@@ -583,81 +525,75 @@ namespace osu.Game.Tests.Visual.Online
InputManager.Click(MouseButton.Left);
}
- private class ChannelManagerContainer : Container
+ private List createChannelMessages(Channel channel)
{
- public TestChatOverlay ChatOverlay { get; private set; }
-
- [Cached]
- public ChannelManager ChannelManager { get; } = new ChannelManager();
-
- private readonly List channels;
-
- public ChannelManagerContainer(List channels)
+ var message = new Message
+ {
+ ChannelId = channel.Id,
+ Content = $"Hello, this is a message in {channel.Name}",
+ Sender = testUser,
+ Timestamp = new DateTimeOffset(DateTime.Now),
+ };
+ return new List { message };
+ }
+
+ private Channel createPublicChannel(int id) => new Channel
+ {
+ Id = id,
+ Name = $"#channel-{id}",
+ Topic = $"We talk about the number {id} here",
+ Type = ChannelType.Public,
+ };
+
+ private Channel createPrivateChannel()
+ {
+ int id = RNG.Next(0, 10000);
+ return new Channel(new APIUser
+ {
+ Id = id,
+ Username = $"test user {id}",
+ });
+ }
+
+ private Channel createAnnounceChannel()
+ {
+ int id = RNG.Next(0, 10000);
+ return new Channel
+ {
+ Name = $"Announce {id}",
+ Type = ChannelType.Announce,
+ Id = id,
+ };
+ }
+
+ private class TestChatOverlay : ChatOverlay
+ {
+ public bool SlowLoading { get; set; }
+
+ public SlowLoadingDrawableChannel GetSlowLoadingChannel(Channel channel) => DrawableChannels.OfType().Single(c => c.Channel == channel);
+
+ protected override ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel)
+ {
+ return SlowLoading
+ ? new SlowLoadingDrawableChannel(newChannel)
+ : new ChatOverlayDrawableChannel(newChannel);
+ }
+ }
+
+ private class SlowLoadingDrawableChannel : ChatOverlayDrawableChannel
+ {
+ public readonly ManualResetEventSlim LoadEvent = new ManualResetEventSlim();
+
+ public SlowLoadingDrawableChannel([NotNull] Channel channel)
+ : base(channel)
{
- this.channels = channels;
}
[BackgroundDependencyLoader]
private void load()
{
- ((BindableList)ChannelManager.AvailableChannels).AddRange(channels);
-
- InternalChildren = new Drawable[]
- {
- ChannelManager,
- ChatOverlay = new TestChatOverlay { RelativeSizeAxes = Axes.Both, },
- };
+ LoadEvent.Wait(10000);
}
}
-
- private class TestChatOverlay : ChatOverlay
- {
- public Visibility SelectionOverlayState => ChannelSelectionOverlay.State.Value;
-
- public new ChannelTabControl ChannelTabControl => base.ChannelTabControl;
-
- public new ChannelSelectionOverlay ChannelSelectionOverlay => base.ChannelSelectionOverlay;
-
- protected override ChannelTabControl CreateChannelTabControl() => new TestTabControl();
-
- public IReadOnlyDictionary> TabMap => ((TestTabControl)ChannelTabControl).TabMap;
- }
-
- private class TestTabControl : ChannelTabControl
- {
- protected override TabItem CreateTabItem(Channel value)
- {
- switch (value.Type)
- {
- case ChannelType.PM:
- return new TestPrivateChannelTabItem(value);
-
- default:
- return new TestChannelTabItem(value);
- }
- }
-
- public new IReadOnlyDictionary> TabMap => base.TabMap;
- }
-
- private class TestChannelTabItem : ChannelTabItem
- {
- public TestChannelTabItem(Channel channel)
- : base(channel)
- {
- }
-
- public new ClickableContainer CloseButton => base.CloseButton;
- }
-
- private class TestPrivateChannelTabItem : PrivateChannelTabItem
- {
- public TestPrivateChannelTabItem(Channel channel)
- : base(channel)
- {
- }
-
- public new ClickableContainer CloseButton => base.CloseButton;
- }
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs
deleted file mode 100644
index e27db00003..0000000000
--- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs
+++ /dev/null
@@ -1,500 +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.Linq;
-using System.Collections.Generic;
-using System.Net;
-using System.Threading;
-using JetBrains.Annotations;
-using NUnit.Framework;
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Logging;
-using osu.Framework.Testing;
-using osu.Framework.Utils;
-using osu.Game.Configuration;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
-using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Online.Chat;
-using osu.Game.Overlays;
-using osu.Game.Overlays.Chat;
-using osu.Game.Overlays.Chat.Listing;
-using osu.Game.Overlays.Chat.ChannelList;
-using osuTK;
-using osuTK.Input;
-
-namespace osu.Game.Tests.Visual.Online
-{
- [TestFixture]
- public class TestSceneChatOverlayV2 : OsuManualInputManagerTestScene
- {
- private TestChatOverlayV2 chatOverlay;
- private ChannelManager channelManager;
-
- private APIUser testUser;
- private Channel testPMChannel;
- private Channel[] testChannels;
-
- private Channel testChannel1 => testChannels[0];
- private Channel testChannel2 => testChannels[1];
-
- [Resolved]
- private OsuConfigManager config { get; set; } = null!;
-
- [SetUp]
- public void SetUp() => Schedule(() =>
- {
- testUser = new APIUser { Username = "test user", Id = 5071479 };
- testPMChannel = new Channel(testUser);
- testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray();
-
- Child = new DependencyProvidingContainer
- {
- RelativeSizeAxes = Axes.Both,
- CachedDependencies = new (Type, object)[]
- {
- (typeof(ChannelManager), channelManager = new ChannelManager()),
- },
- Children = new Drawable[]
- {
- channelManager,
- chatOverlay = new TestChatOverlayV2 { RelativeSizeAxes = Axes.Both },
- },
- };
- });
-
- [SetUpSteps]
- public void SetUpSteps()
- {
- AddStep("Setup request handler", () =>
- {
- ((DummyAPIAccess)API).HandleRequest = req =>
- {
- switch (req)
- {
- case GetUpdatesRequest getUpdates:
- getUpdates.TriggerFailure(new WebException());
- return true;
-
- case JoinChannelRequest joinChannel:
- joinChannel.TriggerSuccess();
- return true;
-
- case LeaveChannelRequest leaveChannel:
- leaveChannel.TriggerSuccess();
- return true;
-
- case GetMessagesRequest getMessages:
- getMessages.TriggerSuccess(createChannelMessages(getMessages.Channel));
- return true;
-
- case GetUserRequest getUser:
- if (getUser.Lookup == testUser.Username)
- getUser.TriggerSuccess(testUser);
- else
- getUser.TriggerFailure(new WebException());
- return true;
-
- case PostMessageRequest postMessage:
- postMessage.TriggerSuccess(new Message(RNG.Next(0, 10000000))
- {
- Content = postMessage.Message.Content,
- ChannelId = postMessage.Message.ChannelId,
- Sender = postMessage.Message.Sender,
- Timestamp = new DateTimeOffset(DateTime.Now),
- });
- return true;
-
- default:
- Logger.Log($"Unhandled Request Type: {req.GetType()}");
- return false;
- }
- };
- });
-
- AddStep("Add test channels", () =>
- {
- (channelManager.AvailableChannels as BindableList)?.AddRange(testChannels);
- });
- }
-
- [Test]
- public void TestBasic()
- {
- AddStep("Show overlay with channel", () =>
- {
- chatOverlay.Show();
- Channel joinedChannel = channelManager.JoinChannel(testChannel1);
- channelManager.CurrentChannel.Value = joinedChannel;
- });
- AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
- AddUntilStep("Channel is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
- }
-
- [Test]
- public void TestShowHide()
- {
- AddStep("Show overlay", () => chatOverlay.Show());
- AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
- AddStep("Hide overlay", () => chatOverlay.Hide());
- AddAssert("Overlay is hidden", () => chatOverlay.State.Value == Visibility.Hidden);
- }
-
- [Test]
- public void TestChatHeight()
- {
- BindableFloat configChatHeight = new BindableFloat();
- config.BindWith(OsuSetting.ChatDisplayHeight, configChatHeight);
- float newHeight = 0;
-
- AddStep("Reset config chat height", () => configChatHeight.SetDefault());
- AddStep("Show overlay", () => chatOverlay.Show());
- AddAssert("Overlay uses config height", () => chatOverlay.Height == configChatHeight.Default);
- AddStep("Click top bar", () =>
- {
- InputManager.MoveMouseTo(chatOverlayTopBar);
- InputManager.PressButton(MouseButton.Left);
- });
- AddStep("Drag overlay to new height", () => InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300)));
- AddStep("Stop dragging", () => InputManager.ReleaseButton(MouseButton.Left));
- AddStep("Store new height", () => newHeight = chatOverlay.Height);
- AddAssert("Config height changed", () => !configChatHeight.IsDefault && configChatHeight.Value == newHeight);
- AddStep("Hide overlay", () => chatOverlay.Hide());
- AddStep("Show overlay", () => chatOverlay.Show());
- AddAssert("Overlay uses new height", () => chatOverlay.Height == newHeight);
- }
-
- [Test]
- public void TestChannelSelection()
- {
- AddStep("Show overlay", () => chatOverlay.Show());
- AddAssert("Listing is visible", () => listingIsVisible);
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
- AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
- AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
- }
-
- [Test]
- public void TestSearchInListing()
- {
- AddStep("Show overlay", () => chatOverlay.Show());
- AddAssert("Listing is visible", () => listingIsVisible);
- AddStep("Search for 'number 2'", () => chatOverlayTextBox.Text = "number 2");
- AddUntilStep("Only channel 2 visibile", () =>
- {
- IEnumerable listingItems = chatOverlay.ChildrenOfType()
- .Where(item => item.IsPresent);
- return listingItems.Count() == 1 && listingItems.Single().Channel == testChannel2;
- });
- }
-
- [Test]
- public void TestChannelCloseButton()
- {
- AddStep("Show overlay", () => chatOverlay.Show());
- AddStep("Join PM and public channels", () =>
- {
- channelManager.JoinChannel(testChannel1);
- channelManager.JoinChannel(testPMChannel);
- });
- AddStep("Select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
- AddStep("Click close button", () =>
- {
- ChannelListItemCloseButton closeButton = getChannelListItem(testPMChannel).ChildrenOfType().Single();
- clickDrawable(closeButton);
- });
- AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(testPMChannel));
- AddStep("Select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
- AddStep("Click close button", () =>
- {
- ChannelListItemCloseButton closeButton = getChannelListItem(testChannel1).ChildrenOfType().Single();
- clickDrawable(closeButton);
- });
- AddAssert("Normal channel closed", () => !channelManager.JoinedChannels.Contains(testChannel1));
- }
-
- [Test]
- public void TestChatCommand()
- {
- AddStep("Show overlay", () => chatOverlay.Show());
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
- AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
- AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}"));
- AddAssert("PM channel is selected", () =>
- channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser);
- AddStep("Open chat with non-existent user", () => channelManager.PostCommand("chat user_doesnt_exist"));
- AddAssert("Last message is error", () => channelManager.CurrentChannel.Value.Messages.Last() is ErrorMessage);
-
- // Make sure no unnecessary requests are made when the PM channel is already open.
- AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
- AddStep("Unregister request handling", () => ((DummyAPIAccess)API).HandleRequest = null);
- AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}"));
- AddAssert("PM channel is selected", () =>
- channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser);
- }
-
- [Test]
- public void TestMultiplayerChannelIsNotShown()
- {
- Channel multiplayerChannel = null;
-
- AddStep("Show overlay", () => chatOverlay.Show());
- AddStep("Join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser())
- {
- Name = "#mp_1",
- Type = ChannelType.Multiplayer,
- }));
- AddAssert("Channel is joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel));
- AddUntilStep("Channel not present in listing", () => !chatOverlay.ChildrenOfType()
- .Where(item => item.IsPresent)
- .Select(item => item.Channel)
- .Contains(multiplayerChannel));
- }
-
- [Test]
- public void TestHighlightOnCurrentChannel()
- {
- Message message = null;
-
- AddStep("Show overlay", () => chatOverlay.Show());
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
- AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
- AddStep("Send message in channel 1", () =>
- {
- testChannel1.AddNewMessages(message = new Message
- {
- ChannelId = testChannel1.Id,
- Content = "Message to highlight!",
- Timestamp = DateTimeOffset.Now,
- Sender = testUser,
- });
- });
- AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
- AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
- }
-
- [Test]
- public void TestHighlightOnAnotherChannel()
- {
- Message message = null;
-
- AddStep("Show overlay", () => chatOverlay.Show());
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
- AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
- AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
- AddStep("Send message in channel 2", () =>
- {
- testChannel2.AddNewMessages(message = new Message
- {
- ChannelId = testChannel2.Id,
- Content = "Message to highlight!",
- Timestamp = DateTimeOffset.Now,
- Sender = testUser,
- });
- });
- AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2));
- AddUntilStep("Channel 2 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2);
- }
-
- [Test]
- public void TestHighlightOnLeftChannel()
- {
- Message message = null;
-
- AddStep("Show overlay", () => chatOverlay.Show());
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
- AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
- AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
- AddStep("Send message in channel 2", () =>
- {
- testChannel2.AddNewMessages(message = new Message
- {
- ChannelId = testChannel2.Id,
- Content = "Message to highlight!",
- Timestamp = DateTimeOffset.Now,
- Sender = testUser,
- });
- });
- AddStep("Leave channel 2", () => channelManager.LeaveChannel(testChannel2));
- AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2));
- AddUntilStep("Channel 2 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2);
- }
-
- [Test]
- public void TestHighlightWhileChatNeverOpen()
- {
- Message message = null;
-
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
- AddStep("Send message in channel 1", () =>
- {
- testChannel1.AddNewMessages(message = new Message
- {
- ChannelId = testChannel1.Id,
- Content = "Message to highlight!",
- Timestamp = DateTimeOffset.Now,
- Sender = testUser,
- });
- });
- AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
- AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
- }
-
- [Test]
- public void TestHighlightWithNullChannel()
- {
- Message message = null;
-
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
- AddStep("Send message in channel 1", () =>
- {
- testChannel1.AddNewMessages(message = new Message
- {
- ChannelId = testChannel1.Id,
- Content = "Message to highlight!",
- Timestamp = DateTimeOffset.Now,
- Sender = testUser,
- });
- });
- AddStep("Set null channel", () => channelManager.CurrentChannel.Value = null);
- AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
- AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
- }
-
- [Test]
- public void TestTextBoxRetainsFocus()
- {
- AddStep("Show overlay", () => chatOverlay.Show());
- AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
- AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
- AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
- AddStep("Click drawable channel", () => clickDrawable(currentDrawableChannel));
- AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
- AddStep("Click selector", () => clickDrawable(channelSelectorButton));
- AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
- AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType().Single()));
- AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
- AddStep("Click channel list", () => clickDrawable(chatOverlay.ChildrenOfType().Single()));
- AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
- AddStep("Click top bar", () => clickDrawable(chatOverlay.ChildrenOfType().Single()));
- AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
- AddStep("Hide overlay", () => chatOverlay.Hide());
- AddAssert("TextBox is not focused", () => InputManager.FocusedDrawable == null);
- }
-
- [Test]
- public void TestSlowLoadingChannel()
- {
- AddStep("Show overlay (slow-loading)", () =>
- {
- chatOverlay.Show();
- chatOverlay.SlowLoading = true;
- });
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
- AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
- AddAssert("Channel 1 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Loading);
-
- AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
- AddStep("Select channel 2", () => clickDrawable(getChannelListItem(testChannel2)));
- AddAssert("Channel 2 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel2).LoadState == LoadState.Loading);
-
- AddStep("Finish channel 1 load", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadEvent.Set());
- AddAssert("Channel 1 ready", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Ready);
- AddAssert("Channel 1 not displayed", () => !channelIsVisible);
-
- AddStep("Finish channel 2 load", () => chatOverlay.GetSlowLoadingChannel(testChannel2).LoadEvent.Set());
- AddAssert("Channel 2 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel2).IsLoaded);
- AddAssert("Channel 2 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2);
-
- AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
- AddAssert("Channel 1 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel1).IsLoaded);
- AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
- }
-
- private bool listingIsVisible =>
- chatOverlay.ChildrenOfType().Single().State.Value == Visibility.Visible;
-
- private bool loadingIsVisible =>
- chatOverlay.ChildrenOfType().Single().State.Value == Visibility.Visible;
-
- private bool channelIsVisible =>
- !listingIsVisible && !loadingIsVisible;
-
- private DrawableChannel currentDrawableChannel =>
- chatOverlay.ChildrenOfType().Single();
-
- private ChannelListItem getChannelListItem(Channel channel) =>
- chatOverlay.ChildrenOfType().Single(item => item.Channel == channel);
-
- private ChatTextBox chatOverlayTextBox =>
- chatOverlay.ChildrenOfType().Single();
-
- private ChatOverlayTopBar chatOverlayTopBar =>
- chatOverlay.ChildrenOfType().Single();
-
- private ChannelListItem channelSelectorButton =>
- chatOverlay.ChildrenOfType().Single(item => item.Channel is ChannelListing.ChannelListingChannel);
-
- private void clickDrawable(Drawable d)
- {
- InputManager.MoveMouseTo(d);
- InputManager.Click(MouseButton.Left);
- }
-
- private List createChannelMessages(Channel channel)
- {
- var message = new Message
- {
- ChannelId = channel.Id,
- Content = $"Hello, this is a message in {channel.Name}",
- Sender = testUser,
- Timestamp = new DateTimeOffset(DateTime.Now),
- };
- return new List { message };
- }
-
- private Channel createPublicChannel(int id) => new Channel
- {
- Id = id,
- Name = $"#channel-{id}",
- Topic = $"We talk about the number {id} here",
- Type = ChannelType.Public,
- };
-
- private class TestChatOverlayV2 : ChatOverlayV2
- {
- public bool SlowLoading { get; set; }
-
- public SlowLoadingDrawableChannel GetSlowLoadingChannel(Channel channel) => DrawableChannels.OfType().Single(c => c.Channel == channel);
-
- protected override ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel)
- {
- return SlowLoading
- ? new SlowLoadingDrawableChannel(newChannel)
- : new ChatOverlayDrawableChannel(newChannel);
- }
- }
-
- private class SlowLoadingDrawableChannel : ChatOverlayDrawableChannel
- {
- public readonly ManualResetEventSlim LoadEvent = new ManualResetEventSlim();
-
- public SlowLoadingDrawableChannel([NotNull] Channel channel)
- : base(channel)
- {
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- LoadEvent.Wait(10000);
- }
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs
index 35a4f8cf2d..edee26c081 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs
@@ -11,6 +11,7 @@ using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Spectator;
+using osu.Game.Overlays;
using osu.Game.Overlays.Dashboard;
using osu.Game.Tests.Visual.Spectator;
using osu.Game.Users;
@@ -42,7 +43,8 @@ namespace osu.Game.Tests.Visual.Online
CachedDependencies = new (Type, object)[]
{
(typeof(SpectatorClient), spectatorClient),
- (typeof(UserLookupCache), lookupCache)
+ (typeof(UserLookupCache), lookupCache),
+ (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Purple)),
},
Child = currentlyPlaying = new CurrentlyPlayingDisplay
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
index 860ef5d565..cb52f41c33 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
@@ -128,11 +128,11 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("Ensure no adjacent day separators", () =>
{
- var indices = chatDisplay.FillFlow.OfType().Select(ds => chatDisplay.FillFlow.IndexOf(ds));
+ var indices = chatDisplay.FillFlow.OfType().Select(ds => chatDisplay.FillFlow.IndexOf(ds));
foreach (int i in indices)
{
- if (i < chatDisplay.FillFlow.Count && chatDisplay.FillFlow[i + 1] is DrawableChannel.DaySeparator)
+ if (i < chatDisplay.FillFlow.Count && chatDisplay.FillFlow[i + 1] is DaySeparator)
return false;
}
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
index f5fe00458a..c532e8bc05 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
@@ -173,6 +173,8 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddUntilStep("wait for scores loaded", () =>
requestComplete
+ // request handler may need to fire more than once to get scores.
+ && totalCount > 0
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
&& resultsScreen.ScorePanelList.AllPanelsVisible);
AddWaitStep("wait for display", 5);
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs
index 35281a85eb..1efe6d7380 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -13,6 +14,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Rulesets.Osu.Objects;
@@ -114,10 +116,7 @@ namespace osu.Game.Tests.Visual.Ranking
throw new NotImplementedException();
}
- public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap)
- {
- throw new NotImplementedException();
- }
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TestBeatmapConverter(beatmap);
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap)
{
@@ -151,6 +150,24 @@ namespace osu.Game.Tests.Visual.Ranking
}
}
};
+
+ private class TestBeatmapConverter : IBeatmapConverter
+ {
+#pragma warning disable CS0067 // The event is never used
+ public event Action> ObjectConverted;
+#pragma warning restore CS0067
+
+ public IBeatmap Beatmap { get; }
+
+ public TestBeatmapConverter(IBeatmap beatmap)
+ {
+ Beatmap = beatmap;
+ }
+
+ public bool CanConvert() => true;
+
+ public IBeatmap Convert(CancellationToken cancellationToken = default) => Beatmap.Clone();
+ }
}
private class TestRulesetAllStatsRequireHitEvents : TestRuleset
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
index 83265e13ad..3e679a7905 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
@@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Framework.Utils;
+using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
@@ -83,7 +84,7 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("clear label", () => textBox.LabelText = default);
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
- AddStep("set warning text", () => textBox.WarningText = "This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...");
+ AddStep("set warning text", () => textBox.SetNoticeText("This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...", true));
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
}
@@ -129,16 +130,18 @@ namespace osu.Game.Tests.Visual.Settings
SettingsNumberBox numberBox = null;
AddStep("create settings item", () => Child = numberBox = new SettingsNumberBox());
- AddAssert("warning text not created", () => !numberBox.ChildrenOfType().Any());
+ AddAssert("warning text not created", () => !numberBox.ChildrenOfType().Any());
- AddStep("set warning text", () => numberBox.WarningText = "this is a warning!");
- AddAssert("warning text created", () => numberBox.ChildrenOfType().Single().Alpha == 1);
+ AddStep("set warning text", () => numberBox.SetNoticeText("this is a warning!", true));
+ AddAssert("warning text created", () => numberBox.ChildrenOfType().Single().Alpha == 1);
- AddStep("unset warning text", () => numberBox.WarningText = default);
- AddAssert("warning text hidden", () => numberBox.ChildrenOfType().Single().Alpha == 0);
+ AddStep("unset warning text", () => numberBox.ClearNoticeText());
+ AddAssert("warning text hidden", () => !numberBox.ChildrenOfType().Any());
- AddStep("set warning text again", () => numberBox.WarningText = "another warning!");
- AddAssert("warning text shown again", () => numberBox.ChildrenOfType().Single().Alpha == 1);
+ AddStep("set warning text again", () => numberBox.SetNoticeText("another warning!", true));
+ AddAssert("warning text shown again", () => numberBox.ChildrenOfType().Single().Alpha == 1);
+
+ AddStep("set non warning text", () => numberBox.SetNoticeText("you did good!"));
}
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs
index 48b5690243..d09efdc925 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs
@@ -185,7 +185,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("step to next", () => overlay.NextButton.TriggerClick());
- AddAssert("is at known screen", () => overlay.CurrentScreen is ScreenBeatmaps);
+ AddAssert("is at known screen", () => overlay.CurrentScreen is ScreenUIScale);
AddStep("hide", () => overlay.Hide());
AddAssert("overlay hidden", () => overlay.State.Value == Visibility.Hidden);
@@ -195,7 +195,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("run notification action", () => lastNotification.Activated());
AddAssert("overlay shown", () => overlay.State.Value == Visibility.Visible);
- AddAssert("is resumed", () => overlay.CurrentScreen is ScreenBeatmaps);
+ AddAssert("is resumed", () => overlay.CurrentScreen is ScreenUIScale);
}
// interface mocks break hot reload, mocking this stub implementation instead works around it.
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index 75e30f76c3..9bb02c3e75 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -435,15 +435,19 @@ namespace osu.Game.Tests.Visual.UserInterface
createScreen();
changeRuleset(0);
+ AddAssert("deselect all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value);
+
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 2);
+ AddAssert("deselect all button enabled", () => this.ChildrenOfType().Single().Enabled.Value);
AddStep("click deselect all button", () =>
{
- InputManager.MoveMouseTo(this.ChildrenOfType().Last());
+ InputManager.MoveMouseTo(this.ChildrenOfType().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
+ AddAssert("deselect all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value);
}
[Test]
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs
index f45c55d912..454a71e6d2 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs
@@ -5,9 +5,12 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
+using osu.Game.Overlays.Settings;
namespace osu.Game.Tests.Visual.UserInterface
{
@@ -15,14 +18,31 @@ namespace osu.Game.Tests.Visual.UserInterface
{
private readonly BindableBool enabled = new BindableBool(true);
- protected override Drawable CreateContent() => new RoundedButton
+ protected override Drawable CreateContent()
{
- Width = 400,
- Text = "Test button",
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Enabled = { BindTarget = enabled },
- };
+ return new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new RoundedButton
+ {
+ Width = 400,
+ Text = "Test button",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Enabled = { BindTarget = enabled },
+ },
+ new SettingsButton
+ {
+ Text = "Test button",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Enabled = { BindTarget = enabled },
+ },
+ }
+ };
+ }
[Test]
public void TestDisabled()
@@ -34,7 +54,8 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestBackgroundColour()
{
AddStep("set red scheme", () => CreateThemedContent(OverlayColourScheme.Red));
- AddAssert("first button has correct colour", () => Cell(0, 1).ChildrenOfType().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Highlight1);
+ AddAssert("rounded button has correct colour", () => Cell(0, 1).ChildrenOfType().First().BackgroundColour == new OsuColour().Blue3);
+ AddAssert("settings button has correct colour", () => Cell(0, 1).ChildrenOfType().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Highlight1);
}
}
}
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 5f7de0d762..dba457c81c 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -319,6 +319,15 @@ namespace osu.Game.Beatmaps
});
}
+ public void DeleteAllVideos()
+ {
+ realm.Write(r =>
+ {
+ var items = r.All().Where(s => !s.DeletePending && !s.Protected);
+ beatmapModelManager.DeleteVideos(items.ToList());
+ });
+ }
+
public void UndeleteAll()
{
realm.Run(r => beatmapModelManager.Undelete(r.All().Where(s => s.DeletePending).ToList()));
diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs
index 4c680bbcc9..277047348e 100644
--- a/osu.Game/Beatmaps/BeatmapModelManager.cs
+++ b/osu.Game/Beatmaps/BeatmapModelManager.cs
@@ -16,6 +16,7 @@ using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Skinning;
using osu.Game.Stores;
+using osu.Game.Overlays.Notifications;
#nullable enable
@@ -33,6 +34,8 @@ namespace osu.Game.Beatmaps
protected override string[] HashableFileTypes => new[] { ".osu" };
+ public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv" };
+
public BeatmapModelManager(RealmAccess realm, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null)
: base(realm, storage, onlineLookupQueue)
{
@@ -114,5 +117,50 @@ namespace osu.Game.Beatmaps
item.CopyChangesToRealm(existing);
});
}
+
+ ///
+ /// Delete videos from a list of beatmaps.
+ /// This will post notifications tracking progress.
+ ///
+ public void DeleteVideos(List items, bool silent = false)
+ {
+ if (items.Count == 0) return;
+
+ var notification = new ProgressNotification
+ {
+ Progress = 0,
+ Text = $"Preparing to delete all {HumanisedModelName} videos...",
+ CompletionText = "No videos found to delete!",
+ State = ProgressNotificationState.Active,
+ };
+
+ if (!silent)
+ PostNotification?.Invoke(notification);
+
+ int i = 0;
+ int deleted = 0;
+
+ foreach (var b in items)
+ {
+ if (notification.State == ProgressNotificationState.Cancelled)
+ // user requested abort
+ return;
+
+ var video = b.Files.FirstOrDefault(f => VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.Ordinal)));
+
+ if (video != null)
+ {
+ DeleteFile(b, video);
+ deleted++;
+ notification.CompletionText = $"Deleted {deleted} {HumanisedModelName} video(s)!";
+ }
+
+ notification.Text = $"Deleting videos from {HumanisedModelName}s ({deleted} deleted)";
+
+ notification.Progress = (float)++i / items.Count;
+ }
+
+ notification.State = ProgressNotificationState.Completed;
+ }
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
index 922439fcb8..3a7c8b2ec0 100644
--- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.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.
+#nullable enable
+
using osu.Framework.Bindables;
using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics;
diff --git a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs
index 84903d381a..5ebdee0b09 100644
--- a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs
+++ b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs
@@ -85,6 +85,8 @@ namespace osu.Game.Beatmaps.Drawables
downloadTrackers.Add(beatmapDownloadTracker);
AddInternal(beatmapDownloadTracker);
+ // Note that this is downloading the beatmaps even if they are already downloaded.
+ // We could rely more on `BeatmapDownloadTracker`'s exposed state to avoid this.
beatmapDownloader.Download(beatmapSet);
}
}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
index 9a17d6dc9b..08aaa8da42 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
protected readonly BeatmapDownloadTracker DownloadTracker;
protected BeatmapCard(APIBeatmapSet beatmapSet, bool allowExpansion = true)
- : base(HoverSampleSet.Submit)
+ : base(HoverSampleSet.Button)
{
Expanded = new BindableBool { Disabled = !allowExpansion };
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs
index 58c1ebee0f..7826d64296 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs
@@ -6,7 +6,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
using osu.Game.Graphics;
@@ -245,10 +244,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
});
if (BeatmapSet.HasVideo)
- leftIconArea.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) });
+ leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(20) });
if (BeatmapSet.HasStoryboard)
- leftIconArea.Add(new IconPill(FontAwesome.Solid.Image) { IconSize = new Vector2(20) });
+ leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) });
if (BeatmapSet.FeaturedInSpotlight)
{
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs
index 3d7e81de21..c1ba521925 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs
@@ -7,7 +7,6 @@ using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
using osu.Game.Graphics;
@@ -226,10 +225,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
});
if (BeatmapSet.HasVideo)
- leftIconArea.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) });
+ leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(20) });
if (BeatmapSet.HasStoryboard)
- leftIconArea.Add(new IconPill(FontAwesome.Solid.Image) { IconSize = new Vector2(20) });
+ leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) });
if (BeatmapSet.FeaturedInSpotlight)
{
diff --git a/osu.Game/Beatmaps/Drawables/Cards/IconPill.cs b/osu.Game/Beatmaps/Drawables/Cards/IconPill.cs
index c55e9c0a28..1b2c5d3ffc 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/IconPill.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/IconPill.cs
@@ -3,14 +3,16 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.Drawables.Cards
{
- public class IconPill : CircularContainer
+ public abstract class IconPill : CircularContainer, IHasTooltip
{
public Vector2 IconSize
{
@@ -20,7 +22,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private readonly Container iconContainer;
- public IconPill(IconUsage icon)
+ protected IconPill(IconUsage icon)
{
AutoSizeAxes = Axes.Both;
Masking = true;
@@ -47,5 +49,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
},
};
}
+
+ public abstract LocalisableString TooltipText { get; }
}
}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/StoryboardIconPill.cs b/osu.Game/Beatmaps/Drawables/Cards/StoryboardIconPill.cs
new file mode 100644
index 0000000000..2ebf9107f5
--- /dev/null
+++ b/osu.Game/Beatmaps/Drawables/Cards/StoryboardIconPill.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+using osu.Game.Resources.Localisation.Web;
+
+namespace osu.Game.Beatmaps.Drawables.Cards
+{
+ public class StoryboardIconPill : IconPill
+ {
+ public StoryboardIconPill()
+ : base(FontAwesome.Solid.Image)
+ {
+ }
+
+ public override LocalisableString TooltipText => BeatmapsetsStrings.ShowInfoStoryboard;
+ }
+}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/VideoIconPill.cs b/osu.Game/Beatmaps/Drawables/Cards/VideoIconPill.cs
new file mode 100644
index 0000000000..b81e18b0dd
--- /dev/null
+++ b/osu.Game/Beatmaps/Drawables/Cards/VideoIconPill.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+using osu.Game.Resources.Localisation.Web;
+
+namespace osu.Game.Beatmaps.Drawables.Cards
+{
+ public class VideoIconPill : IconPill
+ {
+ public VideoIconPill()
+ : base(FontAwesome.Solid.Film)
+ {
+ }
+
+ public override LocalisableString TooltipText => BeatmapsetsStrings.ShowInfoVideo;
+ }
+}
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 0276abc3ff..ff13e61360 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps.Formats
}
catch (Exception e)
{
- Logger.Log($"Failed to process line \"{line}\" into \"{output}\": {e.Message}", LoggingTarget.Runtime, LogLevel.Important);
+ Logger.Log($"Failed to process line \"{line}\" into \"{output}\": {e.Message}");
}
}
}
diff --git a/osu.Game/Database/OnlineLookupCache.cs b/osu.Game/Database/OnlineLookupCache.cs
index 2f98aef58a..506103a2c0 100644
--- a/osu.Game/Database/OnlineLookupCache.cs
+++ b/osu.Game/Database/OnlineLookupCache.cs
@@ -91,7 +91,7 @@ namespace osu.Game.Database
}
}
- private void performLookup()
+ private async Task performLookup()
{
// contains at most 50 unique IDs from tasks, which is used to perform the lookup.
var nextTaskBatch = new Dictionary>>();
@@ -127,7 +127,7 @@ namespace osu.Game.Database
// rather than queueing, we maintain our own single-threaded request stream.
// todo: we probably want retry logic here.
- api.Perform(request);
+ await api.PerformAsync(request).ConfigureAwait(false);
finishPendingTask();
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index dbd3b96763..086ec52d80 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -392,7 +392,7 @@ namespace osu.Game.Database
{
total_writes_async.Value++;
using (var realm = getRealmInstance())
- await realm.WriteAsync(action);
+ await realm.WriteAsync(() => action(realm));
}
///
diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs
index afedf36cad..7fd94b57f3 100644
--- a/osu.Game/Graphics/OsuColour.cs
+++ b/osu.Game/Graphics/OsuColour.cs
@@ -4,6 +4,7 @@
using System;
using osu.Framework.Extensions.Color4Extensions;
using osu.Game.Beatmaps;
+using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@@ -188,6 +189,24 @@ namespace osu.Game.Graphics
}
}
+ ///
+ /// Retrieves the main accent colour for a .
+ ///
+ public Color4? ForRoomCategory(RoomCategory roomCategory)
+ {
+ switch (roomCategory)
+ {
+ case RoomCategory.Spotlight:
+ return SpotlightColour;
+
+ case RoomCategory.FeaturedArtist:
+ return FeaturedArtistColour;
+
+ default:
+ return null;
+ }
+ }
+
///
/// Returns a foreground text colour that is supposed to contrast well with
/// the supplied .
@@ -360,5 +379,8 @@ namespace osu.Game.Graphics
public readonly Color4 ChatBlue = Color4Extensions.FromHex(@"17292e");
public readonly Color4 ContextMenuGray = Color4Extensions.FromHex(@"223034");
+
+ public Color4 SpotlightColour => Green2;
+ public Color4 FeaturedArtistColour => Blue2;
}
}
diff --git a/osu.Game/Graphics/UserInterface/BackButton.cs b/osu.Game/Graphics/UserInterface/BackButton.cs
index 1b564ef1b4..2b59ee0282 100644
--- a/osu.Game/Graphics/UserInterface/BackButton.cs
+++ b/osu.Game/Graphics/UserInterface/BackButton.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Graphics.UserInterface
{
Size = TwoLayerButton.SIZE_EXTENDED;
- Child = button = new TwoLayerButton(HoverSampleSet.Submit)
+ Child = button = new TwoLayerButton
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs
index ad69ec4078..69fbd744c9 100644
--- a/osu.Game/Graphics/UserInterface/DialogButton.cs
+++ b/osu.Game/Graphics/UserInterface/DialogButton.cs
@@ -56,8 +56,8 @@ namespace osu.Game.Graphics.UserInterface
private readonly SpriteText spriteText;
private Vector2 hoverSpacing => new Vector2(3f, 0f);
- public DialogButton()
- : base(HoverSampleSet.Submit)
+ public DialogButton(HoverSampleSet sampleSet = HoverSampleSet.Button)
+ : base(sampleSet)
{
RelativeSizeAxes = Axes.X;
diff --git a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
index 0df69a5b54..1730e1478f 100644
--- a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
+++ b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Graphics.UserInterface
Icon = FontAwesome.Solid.ExternalLinkAlt,
RelativeSizeAxes = Axes.Both
},
- new HoverClickSounds(HoverSampleSet.Submit)
+ new HoverClickSounds()
};
}
diff --git a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs
index 12819840e5..ba253a7c71 100644
--- a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs
+++ b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs
@@ -7,6 +7,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions;
using osu.Framework.Input.Events;
+using osu.Framework.Utils;
using osuTK.Input;
namespace osu.Game.Graphics.UserInterface
@@ -37,7 +38,10 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnClick(ClickEvent e)
{
if (buttons.Contains(e.Button) && Contains(e.ScreenSpaceMousePosition))
- sampleClick?.Play();
+ {
+ sampleClick.Frequency.Value = 0.99 + RNG.NextDouble(0.02);
+ sampleClick.Play();
+ }
return base.OnClick(e);
}
diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs
index a5ea6fcfbf..b88f81a143 100644
--- a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs
+++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs
@@ -10,9 +10,6 @@ namespace osu.Game.Graphics.UserInterface
[Description("default")]
Default,
- [Description("submit")]
- Submit,
-
[Description("button")]
Button,
diff --git a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs
index 4780270f66..ee59da7279 100644
--- a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs
+++ b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs
@@ -13,6 +13,7 @@ namespace osu.Game.Graphics.UserInterface
{
public class ShearedToggleButton : ShearedButton
{
+ private Sample? sampleClick;
private Sample? sampleOff;
private Sample? sampleOn;
@@ -39,8 +40,9 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
- sampleOn = audio.Samples.Get(@"UI/check-on");
- sampleOff = audio.Samples.Get(@"UI/check-off");
+ sampleClick = audio.Samples.Get(@"UI/default-select");
+ sampleOn = audio.Samples.Get(@"UI/dropdown-open");
+ sampleOff = audio.Samples.Get(@"UI/dropdown-close");
}
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
@@ -67,6 +69,8 @@ namespace osu.Game.Graphics.UserInterface
private void playSample()
{
+ sampleClick?.Play();
+
if (Active.Value)
sampleOn?.Play();
else
diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
index 066e1a7978..2353d9e0e8 100644
--- a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
@@ -154,7 +154,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
[BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider? colourProvider, OsuColour osuColour)
{
- background.Colour = colourProvider?.Background4 ?? Color4Extensions.FromHex(@"1c2125");
+ background.Colour = colourProvider?.Background5 ?? Color4Extensions.FromHex(@"1c2125");
descriptionText.Colour = osuColour.Yellow;
}
diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
index 3d09d09833..d9dbf4974b 100644
--- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
@@ -2,11 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System.IO;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
+using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@@ -65,6 +67,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
get
{
+ if (BeatmapModelManager.VIDEO_EXTENSIONS.Contains(File.Extension))
+ return FontAwesome.Regular.FileVideo;
+
switch (File.Extension)
{
case @".ogg":
@@ -77,12 +82,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
case @".png":
return FontAwesome.Regular.FileImage;
- case @".mp4":
- case @".avi":
- case @".mov":
- case @".flv":
- return FontAwesome.Regular.FileVideo;
-
default:
return FontAwesome.Regular.File;
}
diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs
index 3c0c3b69e8..cb8c63371d 100644
--- a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs
@@ -2,13 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
-using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterfaceV2
{
@@ -27,9 +25,12 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
[BackgroundDependencyLoader(true)]
- private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour colours)
+ private void load(OsuColour colours)
{
- DefaultBackgroundColour = overlayColourProvider?.Highlight1 ?? colours.Blue3;
+ // According to flyte, buttons are supposed to have explicit colours for now.
+ // Not sure this is the correct direction, but we haven't decided on an `OverlayColourProvider` stand-in yet.
+ // This is a better default. See `SettingsButton` for an override which uses `OverlayColourProvider`.
+ DefaultBackgroundColour = colours.Blue3;
}
protected override void LoadComplete()
diff --git a/osu.Game/IO/Archives/ZipArchiveReader.cs b/osu.Game/IO/Archives/ZipArchiveReader.cs
index 80dfa104f3..ae2b85da51 100644
--- a/osu.Game/IO/Archives/ZipArchiveReader.cs
+++ b/osu.Game/IO/Archives/ZipArchiveReader.cs
@@ -1,11 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using Microsoft.Toolkit.HighPerformance;
+using osu.Framework.Extensions;
using osu.Framework.IO.Stores;
using SharpCompress.Archives.Zip;
+using SixLabors.ImageSharp.Memory;
namespace osu.Game.IO.Archives
{
@@ -27,15 +31,12 @@ namespace osu.Game.IO.Archives
if (entry == null)
throw new FileNotFoundException();
- // allow seeking
- MemoryStream copy = new MemoryStream();
+ var owner = MemoryAllocator.Default.Allocate((int)entry.Size);
using (Stream s = entry.OpenEntryStream())
- s.CopyTo(copy);
+ s.ReadToFill(owner.Memory.Span);
- copy.Position = 0;
-
- return copy;
+ return new MemoryOwnerMemoryStream(owner);
}
public override void Dispose()
@@ -45,5 +46,48 @@ namespace osu.Game.IO.Archives
}
public override IEnumerable Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames();
+
+ private class MemoryOwnerMemoryStream : Stream
+ {
+ private readonly IMemoryOwner owner;
+ private readonly Stream stream;
+
+ public MemoryOwnerMemoryStream(IMemoryOwner owner)
+ {
+ this.owner = owner;
+
+ stream = owner.Memory.AsStream();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ owner?.Dispose();
+ base.Dispose(disposing);
+ }
+
+ public override void Flush() => stream.Flush();
+
+ public override int Read(byte[] buffer, int offset, int count) => stream.Read(buffer, offset, count);
+
+ public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin);
+
+ public override void SetLength(long value) => stream.SetLength(value);
+
+ public override void Write(byte[] buffer, int offset, int count) => stream.Write(buffer, offset, count);
+
+ public override bool CanRead => stream.CanRead;
+
+ public override bool CanSeek => stream.CanSeek;
+
+ public override bool CanWrite => stream.CanWrite;
+
+ public override long Length => stream.Length;
+
+ public override long Position
+ {
+ get => stream.Position;
+ set => stream.Position = value;
+ }
+ }
}
}
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index 69ea6b00ca..3da5f3212e 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -80,6 +80,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight),
new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode),
new KeyBinding(new[] { InputKey.F5 }, GlobalAction.EditorTestGameplay),
+ new KeyBinding(new[] { InputKey.T }, GlobalAction.EditorTapForBPM),
new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.EditorFlipHorizontally),
new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically),
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.EditorDecreaseDistanceSpacing),
@@ -322,5 +323,8 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DeselectAllMods))]
DeselectAllMods,
+
+ [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTapForBPM))]
+ EditorTapForBPM,
}
}
diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
index e392ae619f..586e29a432 100644
--- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
+++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
@@ -174,6 +174,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString EditorTimingMode => new TranslatableString(getKey(@"editor_timing_mode"), @"Timing mode");
+ ///
+ /// "Tap for BPM"
+ ///
+ public static LocalisableString EditorTapForBPM => new TranslatableString(getKey(@"editor_tap_for_bpm"), @"Tap for BPM");
+
///
/// "Cycle grid display mode"
///
diff --git a/osu.Game/Localisation/LayoutSettingsStrings.cs b/osu.Game/Localisation/LayoutSettingsStrings.cs
new file mode 100644
index 0000000000..b4326b8e39
--- /dev/null
+++ b/osu.Game/Localisation/LayoutSettingsStrings.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Localisation;
+
+namespace osu.Game.Localisation
+{
+ public static class LayoutSettingsStrings
+ {
+ private const string prefix = @"osu.Game.Resources.Localisation.LayoutSettings";
+
+ ///
+ /// "Checking for fullscreen capabilities..."
+ ///
+ public static LocalisableString CheckingForFullscreenCapabilities => new TranslatableString(getKey(@"checking_for_fullscreen_capabilities"), @"Checking for fullscreen capabilities...");
+
+ ///
+ /// "osu! is running exclusive fullscreen, guaranteeing low latency!"
+ ///
+ public static LocalisableString OsuIsRunningExclusiveFullscreen => new TranslatableString(getKey(@"osu_is_running_exclusive_fullscreen"), @"osu! is running exclusive fullscreen, guaranteeing low latency!");
+
+ ///
+ /// "Unable to run exclusive fullscreen. You'll still experience some input latency."
+ ///
+ public static LocalisableString UnableToRunExclusiveFullscreen => new TranslatableString(getKey(@"unable_to_run_exclusive_fullscreen"), @"Unable to run exclusive fullscreen. You'll still experience some input latency.");
+
+ ///
+ /// "Using fullscreen on macOS makes interacting with the menu bar and spaces no longer work, and may lead to freezes if a system dialog is presented. Using borderless is recommended."
+ ///
+ public static LocalisableString FullscreenMacOSNote => new TranslatableString(getKey(@"fullscreen_macos_note"), @"Using fullscreen on macOS makes interacting with the menu bar and spaces no longer work, and may lead to freezes if a system dialog is presented. Using borderless is recommended.");
+
+ private static string getKey(string key) => $@"{prefix}:{key}";
+ }
+}
diff --git a/osu.Game/Localisation/MaintenanceSettingsStrings.cs b/osu.Game/Localisation/MaintenanceSettingsStrings.cs
index a0e1a9ddab..7a04bcd1ca 100644
--- a/osu.Game/Localisation/MaintenanceSettingsStrings.cs
+++ b/osu.Game/Localisation/MaintenanceSettingsStrings.cs
@@ -29,6 +29,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString DeleteAllBeatmaps => new TranslatableString(getKey(@"delete_all_beatmaps"), @"Delete ALL beatmaps");
+ ///
+ /// "Delete ALL beatmap videos"
+ ///
+ public static LocalisableString DeleteAllBeatmapVideos => new TranslatableString(getKey(@"delete_all_beatmap_videos"), @"Delete ALL beatmap videos");
+
///
/// "Import scores from stable"
///
diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index 776ff5fd8f..451ea117d5 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -121,8 +121,16 @@ namespace osu.Game.Online.API
if (isFailing) return;
- Logger.Log($@"Performing request {this}", LoggingTarget.Network);
- WebRequest.Perform();
+ try
+ {
+ Logger.Log($@"Performing request {this}", LoggingTarget.Network);
+ WebRequest.Perform();
+ }
+ catch (OperationCanceledException)
+ {
+ // ignore this. internally Perform is running async and the fail state may have changed since
+ // the last check of `isFailing` above.
+ }
if (isFailing) return;
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index f292e95bd1..d3707d977c 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.cs
@@ -63,12 +63,13 @@ namespace osu.Game.Online.API
public virtual void Queue(APIRequest request)
{
- if (HandleRequest?.Invoke(request) != true)
+ Schedule(() =>
{
- // this will fail due to not receiving an APIAccess, and trigger a failure on the request.
- // this is intended - any request in testing that needs non-failures should use HandleRequest.
- request.Perform(this);
- }
+ if (HandleRequest?.Invoke(request) != true)
+ {
+ request.Fail(new InvalidOperationException($@"{nameof(DummyAPIAccess)} cannot process this request."));
+ }
+ });
}
public void Perform(APIRequest request) => HandleRequest?.Invoke(request);
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index b7d67de04d..31f67bcecc 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -15,7 +15,6 @@ using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Chat.Listing;
-using osu.Game.Overlays.Chat.Tabs;
namespace osu.Game.Online.Chat
{
@@ -134,7 +133,7 @@ namespace osu.Game.Online.Chat
private void currentChannelChanged(ValueChangedEvent e)
{
- bool isSelectorChannel = e.NewValue is ChannelSelectorTabItem.ChannelSelectorTabChannel || e.NewValue is ChannelListing.ChannelListingChannel;
+ bool isSelectorChannel = e.NewValue is ChannelListing.ChannelListingChannel;
if (!isSelectorChannel)
JoinChannel(e.NewValue);
diff --git a/osu.Game/Online/Chat/ChannelType.cs b/osu.Game/Online/Chat/ChannelType.cs
index 151efc4645..bd628e90c4 100644
--- a/osu.Game/Online/Chat/ChannelType.cs
+++ b/osu.Game/Online/Chat/ChannelType.cs
@@ -13,5 +13,6 @@ namespace osu.Game.Online.Chat
PM,
Group,
System,
+ Announce,
}
}
diff --git a/osu.Game/Online/Chat/DrawableLinkCompiler.cs b/osu.Game/Online/Chat/DrawableLinkCompiler.cs
index 8356b36667..3b0d049528 100644
--- a/osu.Game/Online/Chat/DrawableLinkCompiler.cs
+++ b/osu.Game/Online/Chat/DrawableLinkCompiler.cs
@@ -38,7 +38,6 @@ namespace osu.Game.Online.Chat
}
public DrawableLinkCompiler(IEnumerable parts)
- : base(HoverSampleSet.Submit)
{
Parts = parts.ToList();
}
diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs
index b7e1bc999b..f57ffcfe25 100644
--- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs
+++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs
@@ -155,9 +155,6 @@ namespace osu.Game.Online.Chat
{
public Func CreateChatLineAction;
- [Resolved]
- private OsuColour colours { get; set; }
-
public StandAloneDrawableChannel(Channel channel)
: base(channel)
{
@@ -166,25 +163,40 @@ namespace osu.Game.Online.Chat
[BackgroundDependencyLoader]
private void load()
{
+ // TODO: Remove once DrawableChannel & ChatLine padding is fixed
ChatLineFlow.Padding = new MarginPadding { Horizontal = 0 };
}
protected override ChatLine CreateChatLine(Message m) => CreateChatLineAction(m);
- protected override Drawable CreateDaySeparator(DateTimeOffset time) => new DaySeparator(time)
+ protected override DaySeparator CreateDaySeparator(DateTimeOffset time) => new StandAloneDaySeparator(time);
+ }
+
+ protected class StandAloneDaySeparator : DaySeparator
+ {
+ protected override float TextSize => 14;
+ protected override float LineHeight => 1;
+ protected override float Spacing => 10;
+ protected override float DateAlign => 120;
+
+ public StandAloneDaySeparator(DateTimeOffset time)
+ : base(time)
{
- TextSize = 14,
- Colour = colours.Yellow,
- LineHeight = 1,
- Padding = new MarginPadding { Horizontal = 10 },
- Margin = new MarginPadding { Vertical = 5 },
- };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Height = 25;
+ Colour = colours.Yellow;
+ // TODO: Remove once DrawableChannel & ChatLine padding is fixed
+ Padding = new MarginPadding { Horizontal = 10 };
+ }
}
protected class StandAloneMessage : ChatLine
{
protected override float TextSize => 15;
-
protected override float HorizontalPadding => 10;
protected override float MessagePadding => 120;
protected override float TimestampPadding => 50;
diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs
index ca9bf00b23..261724e315 100644
--- a/osu.Game/Online/HubClientConnector.cs
+++ b/osu.Game/Online/HubClientConnector.cs
@@ -20,6 +20,8 @@ namespace osu.Game.Online
{
public class HubClientConnector : IHubClientConnector
{
+ public const string SERVER_SHUTDOWN_MESSAGE = "Server is shutting down.";
+
///
/// Invoked whenever a new hub connection is built, to configure it before it's started.
///
@@ -64,20 +66,28 @@ namespace osu.Game.Online
this.preferMessagePack = preferMessagePack;
apiState.BindTo(api.State);
- apiState.BindValueChanged(state =>
- {
- switch (state.NewValue)
- {
- case APIState.Failing:
- case APIState.Offline:
- Task.Run(() => disconnect(true));
- break;
+ apiState.BindValueChanged(state => connectIfPossible(), true);
+ }
- case APIState.Online:
- Task.Run(connect);
- break;
- }
- }, true);
+ public void Reconnect()
+ {
+ Logger.Log($"{clientName} reconnecting...", LoggingTarget.Network);
+ Task.Run(connectIfPossible);
+ }
+
+ private void connectIfPossible()
+ {
+ switch (apiState.Value)
+ {
+ case APIState.Failing:
+ case APIState.Offline:
+ Task.Run(() => disconnect(true));
+ break;
+
+ case APIState.Online:
+ Task.Run(connect);
+ break;
+ }
}
private async Task connect()
diff --git a/osu.Game/Online/IHubClientConnector.cs b/osu.Game/Online/IHubClientConnector.cs
index d2ceb1f030..b168e4669f 100644
--- a/osu.Game/Online/IHubClientConnector.cs
+++ b/osu.Game/Online/IHubClientConnector.cs
@@ -30,5 +30,10 @@ namespace osu.Game.Online
/// Invoked whenever a new hub connection is built, to configure it before it's started.
///
public Action? ConfigureConnection { get; set; }
+
+ ///
+ /// Reconnect if already connected.
+ ///
+ void Reconnect();
}
}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index cae675b406..c95f3fa579 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -197,6 +197,7 @@ namespace osu.Game.Online.Multiplayer
APIRoom.Playlist.Clear();
APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem));
+ APIRoom.CurrentPlaylistItem.Value = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId);
Debug.Assert(LocalUser != null);
addUserToAPIRoom(LocalUser);
@@ -737,6 +738,7 @@ namespace osu.Game.Online.Multiplayer
APIRoom.Type.Value = Room.Settings.MatchType;
APIRoom.QueueMode.Value = Room.Settings.QueueMode;
APIRoom.AutoStartDuration.Value = Room.Settings.AutoStartDuration;
+ APIRoom.CurrentPlaylistItem.Value = APIRoom.Playlist.Single(item => item.ID == settings.PlaylistItemId);
RoomUpdated?.Invoke();
}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs
index 4729765084..cda313bd0a 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs
@@ -25,12 +25,7 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(exception != null);
- string message = exception is HubException
- // HubExceptions arrive with additional message context added, but we want to display the human readable message:
- // "An unexpected error occurred invoking 'AddPlaylistItem' on the server.InvalidStateException: Can't enqueue more than 3 items at once."
- // We generally use the message field for a user-parseable error (eventually to be replaced), so drop the first part for now.
- ? exception.Message.Substring(exception.Message.IndexOf(':') + 1).Trim()
- : exception.Message;
+ string message = exception.GetHubExceptionMessage() ?? exception.Message;
Logger.Log(message, level: LogLevel.Important);
onError?.Invoke(exception);
@@ -40,5 +35,16 @@ namespace osu.Game.Online.Multiplayer
onSuccess?.Invoke();
}
});
+
+ public static string? GetHubExceptionMessage(this Exception exception)
+ {
+ if (exception is HubException hubException)
+ // HubExceptions arrive with additional message context added, but we want to display the human readable message:
+ // "An unexpected error occurred invoking 'AddPlaylistItem' on the server.InvalidStateException: Can't enqueue more than 3 items at once."
+ // We generally use the message field for a user-parseable error (eventually to be replaced), so drop the first part for now.
+ return hubException.Message.Substring(exception.Message.IndexOf(':') + 1).Trim();
+
+ return null;
+ }
}
}
diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
index 4dc23d8b85..a3423d4189 100644
--- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
@@ -3,10 +3,12 @@
#nullable enable
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -71,14 +73,23 @@ namespace osu.Game.Online.Multiplayer
}
}
- protected override Task JoinRoom(long roomId, string? password = null)
+ protected override async Task JoinRoom(long roomId, string? password = null)
{
if (!IsConnected.Value)
- return Task.FromCanceled(new CancellationToken(true));
+ throw new OperationCanceledException();
Debug.Assert(connection != null);
- return connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoomWithPassword), roomId, password ?? string.Empty);
+ try
+ {
+ return await connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoomWithPassword), roomId, password ?? string.Empty);
+ }
+ catch (HubException exception)
+ {
+ if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE)
+ connector?.Reconnect();
+ throw;
+ }
}
protected override Task LeaveRoomInternal()
diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs
index 6ec884d79c..12091bae88 100644
--- a/osu.Game/Online/Rooms/PlaylistItem.cs
+++ b/osu.Game/Online/Rooms/PlaylistItem.cs
@@ -61,7 +61,13 @@ namespace osu.Game.Online.Rooms
/// Used for serialising to the API.
///
[JsonProperty("beatmap_id")]
- private int onlineBeatmapId => Beatmap.OnlineID;
+ private int onlineBeatmapId
+ {
+ get => Beatmap.OnlineID;
+ // This setter is only required for client-side serialise-then-deserialise operations.
+ // Serialisation is supposed to emit only a `beatmap_id`, but a (non-null) `beatmap` is required on deserialise.
+ set => Beatmap = new APIBeatmap { OnlineID = value };
+ }
///
/// A beatmap representing this playlist item.
diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs
index 60c0503ddd..1933267efd 100644
--- a/osu.Game/Online/Rooms/Room.cs
+++ b/osu.Game/Online/Rooms/Room.cs
@@ -162,6 +162,13 @@ namespace osu.Game.Online.Rooms
Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue));
}
+ ///
+ /// Copies values from another into this one.
+ ///
+ ///
+ /// **Beware**: This will store references between s.
+ ///
+ /// The to copy values from.
public void CopyFrom(Room other)
{
RoomID.Value = other.RoomID.Value;
diff --git a/osu.Game/Online/Rooms/RoomCategory.cs b/osu.Game/Online/Rooms/RoomCategory.cs
index a1dcfa5fd9..bca4d78359 100644
--- a/osu.Game/Online/Rooms/RoomCategory.cs
+++ b/osu.Game/Online/Rooms/RoomCategory.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.ComponentModel;
+
namespace osu.Game.Online.Rooms
{
public enum RoomCategory
@@ -8,5 +10,8 @@ namespace osu.Game.Online.Rooms
// used for osu-web deserialization so names shouldn't be changed.
Normal,
Spotlight,
+
+ [Description("Featured Artist")]
+ FeaturedArtist,
}
}
diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
index 4d6ca0b311..0f77f723db 100644
--- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
+++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
@@ -5,10 +5,12 @@
using System.Diagnostics;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.API;
+using osu.Game.Online.Multiplayer;
namespace osu.Game.Online.Spectator
{
@@ -47,14 +49,23 @@ namespace osu.Game.Online.Spectator
}
}
- protected override Task BeginPlayingInternal(SpectatorState state)
+ protected override async Task BeginPlayingInternal(SpectatorState state)
{
if (!IsConnected.Value)
- return Task.CompletedTask;
+ return;
Debug.Assert(connection != null);
- return connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), state);
+ try
+ {
+ await connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), state);
+ }
+ catch (HubException exception)
+ {
+ if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE)
+ connector?.Reconnect();
+ throw;
+ }
}
protected override Task SendFramesInternal(FrameDataBundle bundle)
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 785881d97a..b91d523151 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using Humanizer;
using JetBrains.Annotations;
+using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
@@ -658,11 +659,14 @@ namespace osu.Game
}
protected override IDictionary GetFrameworkConfigDefaults()
- => new Dictionary
+ {
+ return new Dictionary
{
- // General expectation that osu! starts in fullscreen by default (also gives the most predictable performance)
- { FrameworkSetting.WindowMode, WindowMode.Fullscreen }
+ // General expectation that osu! starts in fullscreen by default (also gives the most predictable performance).
+ // However, macOS is bound to have issues when using exclusive fullscreen as it takes full control away from OS, therefore borderless is default there.
+ { FrameworkSetting.WindowMode, RuntimeInfo.OS == RuntimeInfo.Platform.macOS ? WindowMode.Borderless : WindowMode.Fullscreen }
};
+ }
protected override void LoadComplete()
{
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 666413004a..5dbdf6f602 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -80,6 +80,9 @@ namespace osu.Game
public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild;
+ internal EndpointConfiguration CreateEndpoints() =>
+ UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
+
public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version();
///
@@ -268,7 +271,7 @@ namespace osu.Game
dependencies.Cache(SkinManager = new SkinManager(Storage, realm, Host, Resources, Audio, Scheduler));
dependencies.CacheAs(SkinManager);
- EndpointConfiguration endpoints = UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
+ EndpointConfiguration endpoints = CreateEndpoints();
MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl;
diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
index 1be1321d85..f4f958e4a4 100644
--- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
+++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
@@ -5,6 +5,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
@@ -69,7 +70,7 @@ namespace osu.Game.Overlays.AccountCreation
},
usernameTextBox = new OsuTextBox
{
- PlaceholderText = UsersStrings.LoginUsername,
+ PlaceholderText = UsersStrings.LoginUsername.ToLower(),
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this
},
@@ -91,7 +92,7 @@ namespace osu.Game.Overlays.AccountCreation
},
passwordTextBox = new OsuPasswordTextBox
{
- PlaceholderText = "password",
+ PlaceholderText = UsersStrings.LoginPassword.ToLower(),
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
},
diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs
index 52dfcad2cc..ff43170207 100644
--- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs
+++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Overlays.BeatmapListing
Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular),
Text = LabelFor(Value)
},
- new HoverClickSounds()
+ new HoverClickSounds(HoverSampleSet.TabSelect)
});
Enabled.Value = true;
diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs
index 56efb725cd..e0fc7482f6 100644
--- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs
+++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
+using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@@ -229,6 +230,8 @@ namespace osu.Game.Overlays.BeatmapSet
{
Details.BeatmapInfo = b.NewValue;
externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineID}";
+
+ onlineStatusPill.Status = b.NewValue?.Status ?? BeatmapOnlineStatus.None;
};
}
@@ -272,7 +275,6 @@ namespace osu.Game.Overlays.BeatmapSet
featuredArtist.Alpha = setInfo.NewValue.TrackId != null ? 1 : 0;
onlineStatusPill.FadeIn(500, Easing.OutQuint);
- onlineStatusPill.Status = setInfo.NewValue.Status;
downloadButtonsContainer.FadeIn(transition_duration);
favouriteButton.FadeIn(transition_duration);
diff --git a/osu.Game/Overlays/BeatmapSet/FeaturedArtistBeatmapBadge.cs b/osu.Game/Overlays/BeatmapSet/FeaturedArtistBeatmapBadge.cs
index 4f336d85fc..20ee11c7f6 100644
--- a/osu.Game/Overlays/BeatmapSet/FeaturedArtistBeatmapBadge.cs
+++ b/osu.Game/Overlays/BeatmapSet/FeaturedArtistBeatmapBadge.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Overlays.BeatmapSet
private void load(OsuColour colours)
{
BadgeText = BeatmapsetsStrings.FeaturedArtistBadgeLabel;
- BadgeColour = colours.Blue1;
+ BadgeColour = colours.FeaturedArtistColour;
// todo: add linking support to allow redirecting featured artist badge to corresponding track.
}
}
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
index 591e4cf73e..5f24a6549d 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
@@ -253,7 +253,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
noScoresPlaceholder.Hide();
- if (Beatmap.Value == null || Beatmap.Value.OnlineID <= 0 || (Beatmap.Value.BeatmapSet as IBeatmapSetOnlineInfo)?.Status <= BeatmapOnlineStatus.Pending)
+ if (Beatmap.Value == null || Beatmap.Value.OnlineID <= 0 || (Beatmap.Value.Status <= BeatmapOnlineStatus.Pending))
{
Scores = null;
Hide();
diff --git a/osu.Game/Overlays/BeatmapSet/SpotlightBeatmapBadge.cs b/osu.Game/Overlays/BeatmapSet/SpotlightBeatmapBadge.cs
index 3204f79b21..9c5378a967 100644
--- a/osu.Game/Overlays/BeatmapSet/SpotlightBeatmapBadge.cs
+++ b/osu.Game/Overlays/BeatmapSet/SpotlightBeatmapBadge.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Overlays.BeatmapSet
private void load(OsuColour colours)
{
BadgeText = BeatmapsetsStrings.SpotlightBadgeLabel;
- BadgeColour = colours.Pink1;
+ BadgeColour = colours.SpotlightColour;
// todo: add linking support to allow redirecting spotlight badge to https://osu.ppy.sh/wiki/en/Beatmap_Spotlights.
}
}
diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs
index d1ceae604c..47a2d234d1 100644
--- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs
+++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs
@@ -4,16 +4,20 @@
#nullable enable
using System;
+using System.Linq;
using System.Collections.Generic;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
using osu.Game.Overlays.Chat.Listing;
+using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Chat.ChannelList
{
@@ -22,12 +26,20 @@ namespace osu.Game.Overlays.Chat.ChannelList
public Action? OnRequestSelect;
public Action? OnRequestLeave;
+ public IEnumerable Channels => groupFlow.Children
+ .OfType()
+ .SelectMany(channelGroup => channelGroup.ItemFlow)
+ .Select(item => item.Channel);
+
public readonly ChannelListing.ChannelListingChannel ChannelListingChannel = new ChannelListing.ChannelListingChannel();
private readonly Dictionary channelMap = new Dictionary();
- private ChannelListItemFlow publicChannelFlow = null!;
- private ChannelListItemFlow privateChannelFlow = null!;
+ private OsuScrollContainer scroll = null!;
+ private FillFlowContainer groupFlow = null!;
+ private ChannelGroup announceChannelGroup = null!;
+ private ChannelGroup publicChannelGroup = null!;
+ private ChannelGroup privateChannelGroup = null!;
private ChannelListItem selector = null!;
[BackgroundDependencyLoader]
@@ -40,25 +52,22 @@ namespace osu.Game.Overlays.Chat.ChannelList
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background6,
},
- new OsuScrollContainer
+ scroll = new OsuScrollContainer
{
- Padding = new MarginPadding { Vertical = 7 },
RelativeSizeAxes = Axes.Both,
ScrollbarAnchor = Anchor.TopRight,
ScrollDistance = 35f,
- Child = new FillFlowContainer
+ Child = groupFlow = new FillFlowContainer
{
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
- publicChannelFlow = new ChannelListItemFlow("CHANNELS"),
- selector = new ChannelListItem(ChannelListingChannel)
- {
- Margin = new MarginPadding { Bottom = 10 },
- },
- privateChannelFlow = new ChannelListItemFlow("DIRECT MESSAGES"),
+ announceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper()),
+ publicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper()),
+ selector = new ChannelListItem(ChannelListingChannel),
+ privateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper()),
},
},
},
@@ -76,9 +85,11 @@ namespace osu.Game.Overlays.Chat.ChannelList
item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan);
item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan);
- ChannelListItemFlow flow = getFlowForChannel(channel);
+ FillFlowContainer flow = getFlowForChannel(channel);
channelMap.Add(channel, item);
flow.Add(item);
+
+ updateVisibility();
}
public void RemoveChannel(Channel channel)
@@ -87,10 +98,12 @@ namespace osu.Game.Overlays.Chat.ChannelList
return;
ChannelListItem item = channelMap[channel];
- ChannelListItemFlow flow = getFlowForChannel(channel);
+ FillFlowContainer flow = getFlowForChannel(channel);
channelMap.Remove(channel);
flow.Remove(item);
+
+ updateVisibility();
}
public ChannelListItem GetItem(Channel channel)
@@ -101,35 +114,60 @@ namespace osu.Game.Overlays.Chat.ChannelList
return channelMap[channel];
}
- private ChannelListItemFlow getFlowForChannel(Channel channel)
+ public void ScrollChannelIntoView(Channel channel) => scroll.ScrollIntoView(GetItem(channel));
+
+ private FillFlowContainer getFlowForChannel(Channel channel)
{
switch (channel.Type)
{
case ChannelType.Public:
- return publicChannelFlow;
+ return publicChannelGroup.ItemFlow;
case ChannelType.PM:
- return privateChannelFlow;
+ return privateChannelGroup.ItemFlow;
+
+ case ChannelType.Announce:
+ return announceChannelGroup.ItemFlow;
default:
- throw new ArgumentOutOfRangeException();
+ return publicChannelGroup.ItemFlow;
}
}
- private class ChannelListItemFlow : FillFlowContainer
+ private void updateVisibility()
{
- public ChannelListItemFlow(string label)
+ if (announceChannelGroup.ItemFlow.Children.Count == 0)
+ announceChannelGroup.Hide();
+ else
+ announceChannelGroup.Show();
+ }
+
+ private class ChannelGroup : FillFlowContainer
+ {
+ public readonly FillFlowContainer ItemFlow;
+
+ public ChannelGroup(LocalisableString label)
{
Direction = FillDirection.Vertical;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
+ Padding = new MarginPadding { Top = 8 };
- Add(new OsuSpriteText
+ Children = new Drawable[]
{
- Text = label,
- Margin = new MarginPadding { Left = 18, Bottom = 5 },
- Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
- });
+ new OsuSpriteText
+ {
+ Text = label,
+ Margin = new MarginPadding { Left = 18, Bottom = 5 },
+ Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
+ },
+ ItemFlow = new FillFlowContainer
+ {
+ Direction = FillDirection.Vertical,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ },
+ };
}
}
}
diff --git a/osu.Game/Overlays/Chat/ChatOverlayDrawableChannel.cs b/osu.Game/Overlays/Chat/ChatOverlayDrawableChannel.cs
index 3b47adc4b7..a5d22f678e 100644
--- a/osu.Game/Overlays/Chat/ChatOverlayDrawableChannel.cs
+++ b/osu.Game/Overlays/Chat/ChatOverlayDrawableChannel.cs
@@ -6,10 +6,6 @@
using System;
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.Graphics.Sprites;
using osu.Game.Online.Chat;
namespace osu.Game.Overlays.Chat
@@ -24,85 +20,19 @@ namespace osu.Game.Overlays.Chat
[BackgroundDependencyLoader]
private void load()
{
+ // TODO: Remove once DrawableChannel & ChatLine padding is fixed
ChatLineFlow.Padding = new MarginPadding(0);
}
- protected override Drawable CreateDaySeparator(DateTimeOffset time) => new ChatOverlayDaySeparator(time);
+ protected override DaySeparator CreateDaySeparator(DateTimeOffset time) => new ChatOverlayDaySeparator(time);
- private class ChatOverlayDaySeparator : Container
+ private class ChatOverlayDaySeparator : DaySeparator
{
- private readonly DateTimeOffset time;
-
public ChatOverlayDaySeparator(DateTimeOffset time)
+ : base(time)
{
- this.time = time;
- }
-
- [BackgroundDependencyLoader]
- private void load(OverlayColourProvider colourProvider)
- {
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
- Padding = new MarginPadding { Horizontal = 15, Vertical = 20 };
- Child = new GridContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- ColumnDimensions = new[]
- {
- new Dimension(GridSizeMode.Absolute, 200),
- new Dimension(GridSizeMode.Absolute, 15),
- new Dimension(),
- },
- Content = new[]
- {
- new[]
- {
- new GridContainer
- {
- RelativeSizeAxes = Axes.Both,
- ColumnDimensions = new[]
- {
- new Dimension(),
- new Dimension(GridSizeMode.Absolute, 15),
- new Dimension(GridSizeMode.AutoSize),
- },
- Content = new[]
- {
- new[]
- {
- new Circle
- {
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- Colour = colourProvider.Background5,
- RelativeSizeAxes = Axes.X,
- Height = 2,
- },
- Drawable.Empty(),
- new OsuSpriteText
- {
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- Text = time.ToLocalTime().ToString("dd MMMM yyyy").ToUpper(),
- Font = OsuFont.Torus.With(size: 15, weight: FontWeight.SemiBold),
- Colour = colourProvider.Content1,
- },
- },
- },
- },
- Drawable.Empty(),
- new Circle
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Colour = colourProvider.Background5,
- RelativeSizeAxes = Axes.X,
- Height = 2,
- },
- },
- },
- };
+ // TODO: Remove once DrawableChannel & ChatLine padding is fixed
+ Padding = new MarginPadding { Horizontal = 15 };
}
}
}
diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs
index 3a8cd1fb91..79f22b51f7 100644
--- a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs
+++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
+using osu.Game.Resources.Localisation.Web;
using osuTK;
using osuTK.Graphics;
@@ -58,7 +59,7 @@ namespace osu.Game.Overlays.Chat
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
- Text = "osu!chat",
+ Text = ChatStrings.TitleCompact,
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold),
Margin = new MarginPadding { Bottom = 2f },
},
diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs
index 404d686d91..5100959eeb 100644
--- a/osu.Game/Overlays/Chat/ChatTextBar.cs
+++ b/osu.Game/Overlays/Chat/ChatTextBar.cs
@@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
+using osu.Game.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Overlays.Chat
@@ -140,16 +141,16 @@ namespace osu.Game.Overlays.Chat
switch (newChannel?.Type)
{
- case ChannelType.Public:
- chattingText.Text = $"chatting in {newChannel.Name}";
+ case null:
+ chattingText.Text = string.Empty;
break;
case ChannelType.PM:
- chattingText.Text = $"chatting with {newChannel.Name}";
+ chattingText.Text = ChatStrings.TalkingWith(newChannel.Name);
break;
default:
- chattingText.Text = string.Empty;
+ chattingText.Text = ChatStrings.TalkingIn(newChannel.Name);
break;
}
}, true);
diff --git a/osu.Game/Overlays/Chat/ChatTextBox.cs b/osu.Game/Overlays/Chat/ChatTextBox.cs
index e0f949caba..1ee0e8445f 100644
--- a/osu.Game/Overlays/Chat/ChatTextBox.cs
+++ b/osu.Game/Overlays/Chat/ChatTextBox.cs
@@ -5,6 +5,7 @@
using osu.Framework.Bindables;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Chat
{
@@ -22,7 +23,7 @@ namespace osu.Game.Overlays.Chat
{
bool showSearch = change.NewValue;
- PlaceholderText = showSearch ? "type here to search" : "type here";
+ PlaceholderText = showSearch ? HomeStrings.SearchPlaceholder : ChatStrings.InputPlaceholder;
Text = string.Empty;
}, true);
}
diff --git a/osu.Game/Overlays/Chat/DaySeparator.cs b/osu.Game/Overlays/Chat/DaySeparator.cs
new file mode 100644
index 0000000000..2e3796151c
--- /dev/null
+++ b/osu.Game/Overlays/Chat/DaySeparator.cs
@@ -0,0 +1,105 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+using System;
+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.Graphics.Sprites;
+
+namespace osu.Game.Overlays.Chat
+{
+ public class DaySeparator : Container
+ {
+ protected virtual float TextSize => 15;
+
+ protected virtual float LineHeight => 2;
+
+ protected virtual float DateAlign => 200;
+
+ protected virtual float Spacing => 15;
+
+ private readonly DateTimeOffset time;
+
+ [Resolved(CanBeNull = true)]
+ private OverlayColourProvider? colourProvider { get; set; }
+
+ public DaySeparator(DateTimeOffset time)
+ {
+ this.time = time;
+ Height = 40;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.X;
+ Child = new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ RowDimensions = new[] { new Dimension() },
+ ColumnDimensions = new[]
+ {
+ new Dimension(GridSizeMode.Absolute, DateAlign),
+ new Dimension(GridSizeMode.Absolute, Spacing),
+ new Dimension(),
+ },
+ Content = new[]
+ {
+ new[]
+ {
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ RowDimensions = new[] { new Dimension() },
+ ColumnDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.Absolute, Spacing),
+ new Dimension(GridSizeMode.AutoSize),
+ },
+ Content = new[]
+ {
+ new[]
+ {
+ new Circle
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.X,
+ Height = LineHeight,
+ Colour = colourProvider?.Background5 ?? Colour4.White,
+ },
+ Drawable.Empty(),
+ new OsuSpriteText
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Text = time.ToLocalTime().ToString("dd MMMM yyyy").ToUpper(),
+ Font = OsuFont.Torus.With(size: TextSize, weight: FontWeight.SemiBold),
+ Colour = colourProvider?.Content1 ?? Colour4.White,
+ },
+ }
+ },
+ },
+ Drawable.Empty(),
+ new Circle
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.X,
+ Height = LineHeight,
+ Colour = colourProvider?.Background5 ?? Colour4.White,
+ },
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs
index f2d4a3e301..f0d4d12f4f 100644
--- a/osu.Game/Overlays/Chat/DrawableChannel.cs
+++ b/osu.Game/Overlays/Chat/DrawableChannel.cs
@@ -7,14 +7,9 @@ using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Graphics;
using osu.Game.Graphics.Cursor;
-using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
using osuTK.Graphics;
@@ -40,9 +35,6 @@ namespace osu.Game.Overlays.Chat
}
}
- [Resolved]
- private OsuColour colours { get; set; }
-
public DrawableChannel(Channel channel)
{
Channel = channel;
@@ -67,7 +59,7 @@ namespace osu.Game.Overlays.Chat
Padding = new MarginPadding { Bottom = 5 },
Child = ChatLineFlow = new FillFlowContainer
{
- Padding = new MarginPadding { Left = 20, Right = 20 },
+ Padding = new MarginPadding { Horizontal = 15 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
@@ -121,11 +113,7 @@ namespace osu.Game.Overlays.Chat
protected virtual ChatLine CreateChatLine(Message m) => new ChatLine(m);
- protected virtual Drawable CreateDaySeparator(DateTimeOffset time) => new DaySeparator(time)
- {
- Colour = colours.ChatBlue.Lighten(0.7f),
- Margin = new MarginPadding { Vertical = 10 },
- };
+ protected virtual DaySeparator CreateDaySeparator(DateTimeOffset time) => new DaySeparator(time);
private void newMessagesArrived(IEnumerable newMessages) => Schedule(() =>
{
@@ -203,69 +191,5 @@ namespace osu.Game.Overlays.Chat
});
private IEnumerable chatLines => ChatLineFlow.Children.OfType();
-
- public class DaySeparator : Container
- {
- public float TextSize
- {
- get => text.Font.Size;
- set => text.Font = text.Font.With(size: value);
- }
-
- private float lineHeight = 2;
-
- public float LineHeight
- {
- get => lineHeight;
- set => lineHeight = leftBox.Height = rightBox.Height = value;
- }
-
- private readonly SpriteText text;
- private readonly Box leftBox;
- private readonly Box rightBox;
-
- public DaySeparator(DateTimeOffset time)
- {
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
- Child = new GridContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- ColumnDimensions = new[]
- {
- new Dimension(),
- new Dimension(GridSizeMode.AutoSize),
- new Dimension(),
- },
- RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), },
- Content = new[]
- {
- new Drawable[]
- {
- leftBox = new Box
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.X,
- Height = lineHeight,
- },
- text = new OsuSpriteText
- {
- Margin = new MarginPadding { Horizontal = 10 },
- Text = time.ToLocalTime().ToString("dd MMM yyyy"),
- },
- rightBox = new Box
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.X,
- Height = lineHeight,
- },
- }
- }
- };
- }
- }
}
}
diff --git a/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs b/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs
index 86c81d5d79..2a21d30a4a 100644
--- a/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs
+++ b/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs
@@ -6,6 +6,8 @@
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -16,6 +18,7 @@ using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Chat;
using osuTK;
@@ -32,6 +35,8 @@ namespace osu.Game.Overlays.Chat.Listing
public IEnumerable FilterTerms => new LocalisableString[] { Channel.Name, Channel.Topic ?? string.Empty };
public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); }
+ protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();
+
private Box hoverBox = null!;
private SpriteIcon checkbox = null!;
private OsuSpriteText channelText = null!;
@@ -46,14 +51,20 @@ namespace osu.Game.Overlays.Chat.Listing
private const float vertical_margin = 1.5f;
+ private Sample? sampleJoin;
+ private Sample? sampleLeave;
+
public ChannelListingItem(Channel channel)
{
Channel = channel;
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(AudioManager audio)
{
+ sampleJoin = audio.Samples.Get(@"UI/check-on");
+ sampleLeave = audio.Samples.Get(@"UI/check-off");
+
Masking = true;
CornerRadius = 5;
RelativeSizeAxes = Axes.X;
@@ -156,7 +167,19 @@ namespace osu.Game.Overlays.Chat.Listing
}
}, true);
- Action = () => (channelJoined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(Channel);
+ Action = () =>
+ {
+ if (channelJoined.Value)
+ {
+ OnRequestLeave?.Invoke(Channel);
+ sampleLeave?.Play();
+ }
+ else
+ {
+ OnRequestJoin?.Invoke(Channel);
+ sampleJoin?.Play();
+ }
+ };
}
protected override bool OnHover(HoverEvent e)
diff --git a/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs b/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs
deleted file mode 100644
index 59989ade7b..0000000000
--- a/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs
+++ /dev/null
@@ -1,191 +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 osuTK;
-using osuTK.Graphics;
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Input.Events;
-using osu.Framework.Localisation;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Online.Chat;
-using osu.Game.Graphics.Containers;
-
-namespace osu.Game.Overlays.Chat.Selection
-{
- public class ChannelListItem : OsuClickableContainer, IFilterable
- {
- private const float width_padding = 5;
- private const float channel_width = 150;
- private const float text_size = 15;
- private const float transition_duration = 100;
-
- public readonly Channel Channel;
-
- private readonly Bindable joinedBind = new Bindable();
- private readonly OsuSpriteText name;
- private readonly OsuSpriteText topic;
- private readonly SpriteIcon joinedCheckmark;
-
- private Color4 joinedColour;
- private Color4 topicColour;
- private Color4 hoverColour;
-
- public IEnumerable FilterTerms => new LocalisableString[] { Channel.Name, Channel.Topic ?? string.Empty };
-
- public bool MatchingFilter
- {
- set => this.FadeTo(value ? 1f : 0f, 100);
- }
-
- public bool FilteringActive { get; set; }
-
- public Action OnRequestJoin;
- public Action OnRequestLeave;
-
- public ChannelListItem(Channel channel)
- {
- Channel = channel;
-
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
-
- Action = () => { (channel.Joined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(channel); };
-
- Children = new Drawable[]
- {
- new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Horizontal,
- Children = new Drawable[]
- {
- new Container
- {
- Children = new[]
- {
- joinedCheckmark = new SpriteIcon
- {
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- Icon = FontAwesome.Solid.CheckCircle,
- Size = new Vector2(text_size),
- Shadow = false,
- Margin = new MarginPadding { Right = 10f },
- },
- },
- },
- new Container
- {
- Width = channel_width,
- AutoSizeAxes = Axes.Y,
- Children = new[]
- {
- name = new OsuSpriteText
- {
- Text = channel.ToString(),
- Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold),
- Shadow = false,
- },
- },
- },
- new Container
- {
- RelativeSizeAxes = Axes.X,
- Width = 0.7f,
- AutoSizeAxes = Axes.Y,
- Margin = new MarginPadding { Left = width_padding },
- Children = new[]
- {
- topic = new OsuSpriteText
- {
- Text = channel.Topic,
- Font = OsuFont.GetFont(size: text_size, weight: FontWeight.SemiBold),
- Shadow = false,
- },
- },
- },
- new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
- Margin = new MarginPadding { Left = width_padding },
- Spacing = new Vector2(3f, 0f),
- Children = new Drawable[]
- {
- new SpriteIcon
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Icon = FontAwesome.Solid.User,
- Size = new Vector2(text_size - 2),
- Shadow = false,
- },
- new OsuSpriteText
- {
- Text = @"0",
- Font = OsuFont.GetFont(size: text_size, weight: FontWeight.SemiBold),
- Shadow = false,
- },
- },
- },
- },
- },
- };
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- topicColour = colours.Gray9;
- joinedColour = colours.Blue;
- hoverColour = colours.Yellow;
-
- joinedBind.ValueChanged += joined => updateColour(joined.NewValue);
- joinedBind.BindTo(Channel.Joined);
-
- joinedBind.TriggerChange();
- FinishTransforms(true);
- }
-
- protected override bool OnHover(HoverEvent e)
- {
- if (!Channel.Joined.Value)
- name.FadeColour(hoverColour, 50, Easing.OutQuint);
-
- return base.OnHover(e);
- }
-
- protected override void OnHoverLost(HoverLostEvent e)
- {
- if (!Channel.Joined.Value)
- name.FadeColour(Color4.White, transition_duration);
- }
-
- private void updateColour(bool joined)
- {
- if (joined)
- {
- name.FadeColour(Color4.White, transition_duration);
- joinedCheckmark.FadeTo(1f, transition_duration);
- topic.FadeTo(0.8f, transition_duration);
- topic.FadeColour(Color4.White, transition_duration);
- this.FadeColour(joinedColour, transition_duration);
- }
- else
- {
- joinedCheckmark.FadeTo(0f, transition_duration);
- topic.FadeTo(1f, transition_duration);
- topic.FadeColour(topicColour, transition_duration);
- this.FadeColour(Color4.White, transition_duration);
- }
- }
- }
-}
diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSection.cs b/osu.Game/Overlays/Chat/Selection/ChannelSection.cs
deleted file mode 100644
index 070332180c..0000000000
--- a/osu.Game/Overlays/Chat/Selection/ChannelSection.cs
+++ /dev/null
@@ -1,58 +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.Linq;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Localisation;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Online.Chat;
-using osuTK;
-
-namespace osu.Game.Overlays.Chat.Selection
-{
- public class ChannelSection : Container, IHasFilterableChildren
- {
- public readonly FillFlowContainer ChannelFlow;
-
- public IEnumerable FilterableChildren => ChannelFlow.Children;
- public IEnumerable FilterTerms => Array.Empty();
-
- public bool MatchingFilter
- {
- set => this.FadeTo(value ? 1f : 0f, 100);
- }
-
- public bool FilteringActive { get; set; }
-
- public IEnumerable Channels
- {
- set => ChannelFlow.ChildrenEnumerable = value.Select(c => new ChannelListItem(c));
- }
-
- public ChannelSection()
- {
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
-
- Children = new Drawable[]
- {
- new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold),
- Text = "All Channels".ToUpperInvariant()
- },
- ChannelFlow = new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Margin = new MarginPadding { Top = 25 },
- Spacing = new Vector2(0f, 5f),
- },
- };
- }
- }
-}
diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs
deleted file mode 100644
index 9b0354e264..0000000000
--- a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs
+++ /dev/null
@@ -1,194 +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 osuTK;
-using osuTK.Graphics;
-using osu.Framework.Allocation;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Input.Events;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Backgrounds;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Online.Chat;
-using osu.Game.Graphics.Containers;
-
-namespace osu.Game.Overlays.Chat.Selection
-{
- public class ChannelSelectionOverlay : WaveOverlayContainer
- {
- public new const float WIDTH_PADDING = 170;
-
- private const float transition_duration = 500;
-
- private readonly Box bg;
- private readonly Triangles triangles;
- private readonly Box headerBg;
- private readonly SearchTextBox search;
- private readonly SearchContainer sectionsFlow;
-
- protected override bool DimMainContent => false;
-
- public Action OnRequestJoin;
- public Action OnRequestLeave;
-
- public ChannelSelectionOverlay()
- {
- RelativeSizeAxes = Axes.X;
-
- Waves.FirstWaveColour = Color4Extensions.FromHex("353535");
- Waves.SecondWaveColour = Color4Extensions.FromHex("434343");
- Waves.ThirdWaveColour = Color4Extensions.FromHex("515151");
- Waves.FourthWaveColour = Color4Extensions.FromHex("595959");
-
- Children = new Drawable[]
- {
- new Container
- {
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- Children = new Drawable[]
- {
- bg = new Box
- {
- RelativeSizeAxes = Axes.Both,
- },
- triangles = new Triangles
- {
- RelativeSizeAxes = Axes.Both,
- TriangleScale = 5,
- },
- },
- },
- new Container
- {
- RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Top = 85, Right = WIDTH_PADDING },
- Children = new[]
- {
- new OsuScrollContainer
- {
- RelativeSizeAxes = Axes.Both,
- Children = new[]
- {
- sectionsFlow = new SearchContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- LayoutDuration = 200,
- LayoutEasing = Easing.OutQuint,
- Spacing = new Vector2(0f, 20f),
- Padding = new MarginPadding { Vertical = 20, Left = WIDTH_PADDING },
- },
- },
- },
- },
- },
- new Container
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Children = new Drawable[]
- {
- headerBg = new Box
- {
- RelativeSizeAxes = Axes.Both,
- },
- new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(0f, 10f),
- Padding = new MarginPadding { Top = 10f, Bottom = 10f, Left = WIDTH_PADDING, Right = WIDTH_PADDING },
- Children = new Drawable[]
- {
- new OsuSpriteText
- {
- Text = @"Chat Channels",
- Font = OsuFont.GetFont(size: 20),
- Shadow = false,
- },
- search = new HeaderSearchTextBox { RelativeSizeAxes = Axes.X },
- },
- },
- },
- },
- };
-
- search.Current.ValueChanged += term => sectionsFlow.SearchTerm = term.NewValue;
- }
-
- public void UpdateAvailableChannels(IEnumerable channels)
- {
- Scheduler.Add(() =>
- {
- sectionsFlow.ChildrenEnumerable = new[]
- {
- new ChannelSection { Channels = channels, },
- };
-
- foreach (ChannelSection s in sectionsFlow.Children)
- {
- foreach (ChannelListItem c in s.ChannelFlow.Children)
- {
- c.OnRequestJoin = channel => { OnRequestJoin?.Invoke(channel); };
- c.OnRequestLeave = channel => { OnRequestLeave?.Invoke(channel); };
- }
- }
- });
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- bg.Colour = colours.Gray3;
- triangles.ColourDark = colours.Gray3;
- triangles.ColourLight = Color4Extensions.FromHex(@"353535");
-
- headerBg.Colour = colours.Gray2.Opacity(0.75f);
- }
-
- protected override void OnFocus(FocusEvent e)
- {
- search.TakeFocus();
- base.OnFocus(e);
- }
-
- protected override void PopIn()
- {
- if (Alpha == 0) this.MoveToY(DrawHeight);
-
- this.FadeIn(transition_duration, Easing.OutQuint);
- this.MoveToY(0, transition_duration, Easing.OutQuint);
-
- search.HoldFocus = true;
- base.PopIn();
- }
-
- protected override void PopOut()
- {
- this.FadeOut(transition_duration, Easing.InSine);
- this.MoveToY(DrawHeight, transition_duration, Easing.InSine);
-
- search.HoldFocus = false;
- base.PopOut();
- }
-
- private class HeaderSearchTextBox : BasicSearchTextBox
- {
- [BackgroundDependencyLoader]
- private void load()
- {
- BackgroundFocused = Color4.Black.Opacity(0.2f);
- BackgroundUnfocused = Color4.Black.Opacity(0.2f);
- }
- }
- }
-}
diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs
deleted file mode 100644
index e3ede04edd..0000000000
--- a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs
+++ /dev/null
@@ -1,46 +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 osu.Framework.Allocation;
-using osu.Game.Graphics;
-using osu.Game.Online.Chat;
-
-namespace osu.Game.Overlays.Chat.Tabs
-{
- public class ChannelSelectorTabItem : ChannelTabItem
- {
- public override bool IsRemovable => false;
-
- public override bool IsSwitchable => false;
-
- protected override bool IsBoldWhenActive => false;
-
- public ChannelSelectorTabItem()
- : base(new ChannelSelectorTabChannel())
- {
- Depth = float.MaxValue;
- Width = 45;
-
- Icon.Alpha = 0;
-
- Text.Font = Text.Font.With(size: 45);
- Text.Truncate = false;
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colour)
- {
- BackgroundInactive = colour.Gray2;
- BackgroundActive = colour.Gray3;
- }
-
- public class ChannelSelectorTabChannel : Channel
- {
- public ChannelSelectorTabChannel()
- {
- Name = "+";
- Type = ChannelType.System;
- }
- }
- }
-}
diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs
deleted file mode 100644
index c0de093425..0000000000
--- a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs
+++ /dev/null
@@ -1,114 +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 osu.Framework.Graphics;
-using osu.Framework.Graphics.UserInterface;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Online.Chat;
-using osuTK;
-using System;
-using System.Linq;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics.Containers;
-
-namespace osu.Game.Overlays.Chat.Tabs
-{
- public class ChannelTabControl : OsuTabControl
- {
- public const float SHEAR_WIDTH = 10;
-
- public Action OnRequestLeave;
-
- public readonly Bindable ChannelSelectorActive = new Bindable();
-
- private readonly ChannelSelectorTabItem selectorTab;
-
- public ChannelTabControl()
- {
- Padding = new MarginPadding { Left = 50 };
-
- TabContainer.Spacing = new Vector2(-SHEAR_WIDTH, 0);
- TabContainer.Masking = false;
-
- AddTabItem(selectorTab = new ChannelSelectorTabItem());
-
- ChannelSelectorActive.BindTo(selectorTab.Active);
- }
-
- protected override void AddTabItem(TabItem item, bool addToDropdown = true)
- {
- if (item != selectorTab && TabContainer.GetLayoutPosition(selectorTab) < float.MaxValue)
- // performTabSort might've made selectorTab's position wonky, fix it
- TabContainer.SetLayoutPosition(selectorTab, float.MaxValue);
-
- ((ChannelTabItem)item).OnRequestClose += channelItem => OnRequestLeave?.Invoke(channelItem.Value);
-
- base.AddTabItem(item, addToDropdown);
- }
-
- protected override TabItem CreateTabItem(Channel value)
- {
- switch (value.Type)
- {
- default:
- return new ChannelTabItem(value);
-
- case ChannelType.PM:
- return new PrivateChannelTabItem(value);
- }
- }
-
- ///
- /// Adds a channel to the ChannelTabControl.
- /// The first channel added will automaticly selected.
- ///
- /// The channel that is going to be added.
- public void AddChannel(Channel channel)
- {
- if (!Items.Contains(channel))
- AddItem(channel);
-
- Current.Value ??= channel;
- }
-
- ///
- /// Removes a channel from the ChannelTabControl.
- /// If the selected channel is the one that is being removed, the next available channel will be selected.
- ///
- /// The channel that is going to be removed.
- public void RemoveChannel(Channel channel)
- {
- RemoveItem(channel);
-
- if (SelectedTab == null)
- SelectChannelSelectorTab();
- }
-
- public void SelectChannelSelectorTab() => SelectTab(selectorTab);
-
- protected override void SelectTab(TabItem tab)
- {
- if (tab is ChannelSelectorTabItem)
- {
- tab.Active.Value = true;
- return;
- }
-
- base.SelectTab(tab);
- selectorTab.Active.Value = false;
- }
-
- protected override TabFillFlowContainer CreateTabFlow() => new ChannelTabFillFlowContainer
- {
- Direction = FillDirection.Full,
- RelativeSizeAxes = Axes.Both,
- Depth = -1,
- Masking = true
- };
-
- private class ChannelTabFillFlowContainer : TabFillFlowContainer
- {
- protected override int Compare(Drawable x, Drawable y) => CompareReverseChildID(x, y);
- }
- }
-}
diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs
deleted file mode 100644
index 9d2cd8a21d..0000000000
--- a/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs
+++ /dev/null
@@ -1,238 +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 osu.Framework.Allocation;
-using osu.Framework.Audio;
-using osu.Framework.Audio.Sample;
-using osu.Framework.Extensions;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Effects;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.UserInterface;
-using osu.Framework.Input.Events;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Online.Chat;
-using osuTK;
-using osuTK.Graphics;
-using osuTK.Input;
-
-namespace osu.Game.Overlays.Chat.Tabs
-{
- public class ChannelTabItem : TabItem
- {
- protected Color4 BackgroundInactive;
- private Color4 backgroundHover;
- protected Color4 BackgroundActive;
-
- public override bool IsRemovable => !Pinned;
-
- protected readonly SpriteText Text;
- protected readonly ClickableContainer CloseButton;
- private readonly Box box;
- private readonly Box highlightBox;
- protected readonly SpriteIcon Icon;
-
- public Action OnRequestClose;
- private readonly Container content;
-
- protected override Container Content => content;
-
- private Sample sampleTabSwitched;
-
- public ChannelTabItem(Channel value)
- : base(value)
- {
- Width = 150;
-
- RelativeSizeAxes = Axes.Y;
-
- Anchor = Anchor.BottomLeft;
- Origin = Anchor.BottomLeft;
-
- Shear = new Vector2(ChannelTabControl.SHEAR_WIDTH / ChatOverlay.TAB_AREA_HEIGHT, 0);
-
- Masking = true;
-
- InternalChildren = new Drawable[]
- {
- box = new Box
- {
- EdgeSmoothness = new Vector2(1, 0),
- RelativeSizeAxes = Axes.Both,
- },
- highlightBox = new Box
- {
- Width = 5,
- Alpha = 0,
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- EdgeSmoothness = new Vector2(1, 0),
- RelativeSizeAxes = Axes.Y,
- },
- content = new Container
- {
- Shear = new Vector2(-ChannelTabControl.SHEAR_WIDTH / ChatOverlay.TAB_AREA_HEIGHT, 0),
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- Icon = new SpriteIcon
- {
- Icon = DisplayIcon,
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Colour = Color4.Black,
- X = -10,
- Alpha = 0.2f,
- Size = new Vector2(ChatOverlay.TAB_AREA_HEIGHT),
- },
- Text = new OsuSpriteText
- {
- Origin = Anchor.CentreLeft,
- Anchor = Anchor.CentreLeft,
- Text = value.ToString(),
- Font = OsuFont.GetFont(size: 18),
- Padding = new MarginPadding(5)
- {
- Left = LeftTextPadding,
- Right = RightTextPadding,
- },
- RelativeSizeAxes = Axes.X,
- Truncate = true,
- },
- CloseButton = new TabCloseButton
- {
- Alpha = 0,
- Margin = new MarginPadding { Right = 20 },
- Origin = Anchor.CentreRight,
- Anchor = Anchor.CentreRight,
- Action = delegate
- {
- if (IsRemovable) OnRequestClose?.Invoke(this);
- },
- },
- },
- },
- new HoverSounds()
- };
- }
-
- protected virtual float LeftTextPadding => 5;
-
- protected virtual float RightTextPadding => IsRemovable ? 40 : 5;
-
- protected virtual IconUsage DisplayIcon => FontAwesome.Solid.Hashtag;
-
- protected virtual bool ShowCloseOnHover => true;
-
- protected virtual bool IsBoldWhenActive => true;
-
- protected override bool OnHover(HoverEvent e)
- {
- if (IsRemovable && ShowCloseOnHover)
- CloseButton.FadeIn(200, Easing.OutQuint);
-
- if (!Active.Value)
- box.FadeColour(backgroundHover, TRANSITION_LENGTH, Easing.OutQuint);
- return true;
- }
-
- protected override void OnHoverLost(HoverLostEvent e)
- {
- CloseButton.FadeOut(200, Easing.OutQuint);
- updateState();
- }
-
- protected override void OnMouseUp(MouseUpEvent e)
- {
- switch (e.Button)
- {
- case MouseButton.Middle:
- CloseButton.TriggerClick();
- break;
- }
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours, AudioManager audio)
- {
- BackgroundActive = colours.ChatBlue;
- BackgroundInactive = colours.Gray4;
- backgroundHover = colours.Gray7;
- sampleTabSwitched = audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select");
-
- highlightBox.Colour = colours.Yellow;
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- updateState();
- FinishTransforms(true);
- }
-
- private void updateState()
- {
- if (Active.Value)
- FadeActive();
- else
- FadeInactive();
- }
-
- protected const float TRANSITION_LENGTH = 400;
-
- private readonly EdgeEffectParameters activateEdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Shadow,
- Radius = 15,
- Colour = Color4.Black.Opacity(0.4f),
- };
-
- private readonly EdgeEffectParameters deactivateEdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Shadow,
- Radius = 10,
- Colour = Color4.Black.Opacity(0.2f),
- };
-
- protected virtual void FadeActive()
- {
- this.ResizeHeightTo(1.1f, TRANSITION_LENGTH, Easing.OutQuint);
-
- TweenEdgeEffectTo(activateEdgeEffect, TRANSITION_LENGTH);
-
- box.FadeColour(BackgroundActive, TRANSITION_LENGTH, Easing.OutQuint);
- highlightBox.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
-
- if (IsBoldWhenActive) Text.Font = Text.Font.With(weight: FontWeight.Bold);
- }
-
- protected virtual void FadeInactive()
- {
- this.ResizeHeightTo(1, TRANSITION_LENGTH, Easing.OutQuint);
-
- TweenEdgeEffectTo(deactivateEdgeEffect, TRANSITION_LENGTH);
-
- box.FadeColour(IsHovered ? backgroundHover : BackgroundInactive, TRANSITION_LENGTH, Easing.OutQuint);
- highlightBox.FadeOut(TRANSITION_LENGTH, Easing.OutQuint);
-
- Text.Font = Text.Font.With(weight: FontWeight.Medium);
- }
-
- protected override void OnActivated()
- {
- if (IsLoaded)
- sampleTabSwitched?.Play();
-
- updateState();
- }
-
- protected override void OnDeactivated() => updateState();
- }
-}
diff --git a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs
deleted file mode 100644
index d01aec630e..0000000000
--- a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs
+++ /dev/null
@@ -1,95 +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.Linq;
-using osu.Framework.Allocation;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Graphics;
-using osu.Game.Online.Chat;
-using osu.Game.Users.Drawables;
-using osuTK;
-
-namespace osu.Game.Overlays.Chat.Tabs
-{
- public class PrivateChannelTabItem : ChannelTabItem
- {
- protected override IconUsage DisplayIcon => FontAwesome.Solid.At;
-
- public PrivateChannelTabItem(Channel value)
- : base(value)
- {
- if (value.Type != ChannelType.PM)
- throw new ArgumentException("Argument value needs to have the targettype user!");
-
- DrawableAvatar avatar;
-
- AddRange(new Drawable[]
- {
- new Container
- {
- RelativeSizeAxes = Axes.Y,
- AutoSizeAxes = Axes.X,
- Margin = new MarginPadding
- {
- Horizontal = 3
- },
- Origin = Anchor.BottomLeft,
- Anchor = Anchor.BottomLeft,
- Children = new Drawable[]
- {
- new CircularContainer
- {
- Scale = new Vector2(0.95f),
- Size = new Vector2(ChatOverlay.TAB_AREA_HEIGHT),
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Masking = true,
- Child = new DelayedLoadWrapper(avatar = new DrawableAvatar(value.Users.First())
- {
- RelativeSizeAxes = Axes.Both
- })
- {
- RelativeSizeAxes = Axes.Both,
- }
- },
- }
- },
- });
-
- avatar.OnLoadComplete += d => d.FadeInFromZero(300, Easing.OutQuint);
- }
-
- protected override float LeftTextPadding => base.LeftTextPadding + ChatOverlay.TAB_AREA_HEIGHT;
-
- protected override bool ShowCloseOnHover => false;
-
- protected override void FadeActive()
- {
- base.FadeActive();
-
- this.ResizeWidthTo(200, TRANSITION_LENGTH, Easing.OutQuint);
- CloseButton.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
- }
-
- protected override void FadeInactive()
- {
- base.FadeInactive();
-
- this.ResizeWidthTo(ChatOverlay.TAB_AREA_HEIGHT + 10, TRANSITION_LENGTH, Easing.OutQuint);
- CloseButton.FadeOut(TRANSITION_LENGTH, Easing.OutQuint);
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- var user = Value.Users.First();
-
- BackgroundActive = user.Colour != null ? Color4Extensions.FromHex(user.Colour) : colours.BlueDark;
- BackgroundInactive = BackgroundActive.Darken(0.5f);
- }
- }
-}
diff --git a/osu.Game/Overlays/Chat/Tabs/TabCloseButton.cs b/osu.Game/Overlays/Chat/Tabs/TabCloseButton.cs
deleted file mode 100644
index 178afda5ac..0000000000
--- a/osu.Game/Overlays/Chat/Tabs/TabCloseButton.cs
+++ /dev/null
@@ -1,55 +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 osu.Framework.Graphics;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Input.Events;
-using osu.Game.Graphics.Containers;
-using osuTK;
-using osuTK.Graphics;
-
-namespace osu.Game.Overlays.Chat.Tabs
-{
- public class TabCloseButton : OsuClickableContainer
- {
- private readonly SpriteIcon icon;
-
- public TabCloseButton()
- {
- Size = new Vector2(20);
-
- Child = icon = new SpriteIcon
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Scale = new Vector2(0.75f),
- Icon = FontAwesome.Solid.TimesCircle,
- RelativeSizeAxes = Axes.Both,
- };
- }
-
- protected override bool OnMouseDown(MouseDownEvent e)
- {
- icon.ScaleTo(0.5f, 1000, Easing.OutQuint);
- return base.OnMouseDown(e);
- }
-
- protected override void OnMouseUp(MouseUpEvent e)
- {
- icon.ScaleTo(0.75f, 1000, Easing.OutElastic);
- base.OnMouseUp(e);
- }
-
- protected override bool OnHover(HoverEvent e)
- {
- icon.FadeColour(Color4.Red, 200, Easing.OutQuint);
- return base.OnHover(e);
- }
-
- protected override void OnHoverLost(HoverLostEvent e)
- {
- icon.FadeColour(Color4.White, 200, Easing.OutQuint);
- base.OnHoverLost(e);
- }
- }
-}
diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs
index 034670cf37..02769b5d68 100644
--- a/osu.Game/Overlays/ChatOverlay.cs
+++ b/osu.Game/Overlays/ChatOverlay.cs
@@ -1,35 +1,29 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
-using osuTK;
-using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.UserInterface;
-using osu.Framework.Input.Events;
-using osu.Game.Configuration;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Online.Chat;
-using osu.Game.Overlays.Chat;
-using osu.Game.Overlays.Chat.Selection;
-using osu.Game.Overlays.Chat.Tabs;
-using osuTK.Input;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Framework.Localisation;
+using osu.Game.Configuration;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
-using osu.Game.Online;
+using osu.Game.Online.Chat;
+using osu.Game.Overlays.Chat;
+using osu.Game.Overlays.Chat.ChannelList;
+using osu.Game.Overlays.Chat.Listing;
namespace osu.Game.Overlays
{
@@ -39,271 +33,147 @@ namespace osu.Game.Overlays
public LocalisableString Title => ChatStrings.HeaderTitle;
public LocalisableString Description => ChatStrings.HeaderDescription;
- private const float text_box_height = 60;
- private const float channel_selection_min_height = 0.3f;
+ private ChatOverlayTopBar topBar = null!;
+ private ChannelList channelList = null!;
+ private LoadingLayer loading = null!;
+ private ChannelListing channelListing = null!;
+ private ChatTextBar textBar = null!;
+ private Container currentChannelContainer = null!;
- [Resolved]
- private ChannelManager channelManager { get; set; }
+ private readonly Dictionary loadedChannels = new Dictionary();
- private Container currentChannelContainer;
+ protected IEnumerable DrawableChannels => loadedChannels.Values;
- private readonly List loadedChannels = new List();
-
- private LoadingSpinner loading;
-
- private FocusedTextBox textBox;
-
- private const int transition_length = 500;
+ private readonly BindableFloat chatHeight = new BindableFloat();
+ private bool isDraggingTopBar;
+ private float dragStartChatHeight;
public const float DEFAULT_HEIGHT = 0.4f;
- public const float TAB_AREA_HEIGHT = 50;
+ private const int transition_length = 500;
+ private const float top_bar_height = 40;
+ private const float side_bar_width = 190;
+ private const float chat_bar_height = 60;
- protected ChannelTabControl ChannelTabControl;
+ [Resolved]
+ private OsuConfigManager config { get; set; } = null!;
- protected virtual ChannelTabControl CreateChannelTabControl() => new ChannelTabControl();
+ [Resolved]
+ private ChannelManager channelManager { get; set; } = null!;
- private Container chatContainer;
- private TabsArea tabsArea;
- private Box chatBackground;
- private Box tabBackground;
+ [Cached]
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
- public Bindable ChatHeight { get; set; }
-
- private Container channelSelectionContainer;
- protected ChannelSelectionOverlay ChannelSelectionOverlay;
+ [Cached]
+ private readonly Bindable currentChannel = new Bindable();
private readonly IBindableList availableChannels = new BindableList();
private readonly IBindableList joinedChannels = new BindableList();
- private readonly Bindable currentChannel = new Bindable();
-
- public override bool Contains(Vector2 screenSpacePos) => chatContainer.ReceivePositionalInputAt(screenSpacePos)
- || (ChannelSelectionOverlay.State.Value == Visibility.Visible && ChannelSelectionOverlay.ReceivePositionalInputAt(screenSpacePos));
public ChatOverlay()
{
+ Height = DEFAULT_HEIGHT;
+
+ Masking = true;
+
+ const float corner_radius = 7f;
+
+ CornerRadius = corner_radius;
+
+ // Hack to hide the bottom edge corner radius off-screen.
+ Margin = new MarginPadding { Bottom = -corner_radius };
+ Padding = new MarginPadding { Bottom = corner_radius };
+
RelativeSizeAxes = Axes.Both;
- RelativePositionAxes = Axes.Both;
- Anchor = Anchor.BottomLeft;
- Origin = Anchor.BottomLeft;
+ Anchor = Anchor.BottomCentre;
+ Origin = Anchor.BottomCentre;
}
[BackgroundDependencyLoader]
- private void load(OsuConfigManager config, OsuColour colours, TextureStore textures)
+ private void load()
{
- const float padding = 5;
+ // Required for the pop in/out animation
+ RelativePositionAxes = Axes.Both;
Children = new Drawable[]
{
- channelSelectionContainer = new Container
+ topBar = new ChatOverlayTopBar
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = top_bar_height,
+ },
+ channelList = new ChannelList
+ {
+ RelativeSizeAxes = Axes.Y,
+ Width = side_bar_width,
+ Padding = new MarginPadding { Top = top_bar_height },
+ },
+ new Container
{
RelativeSizeAxes = Axes.Both,
- Height = 1f - DEFAULT_HEIGHT,
- Masking = true,
- Children = new[]
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ Padding = new MarginPadding
{
- ChannelSelectionOverlay = new ChannelSelectionOverlay
+ Top = top_bar_height,
+ Left = side_bar_width,
+ Bottom = chat_bar_height,
+ },
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background4,
+ },
+ currentChannelContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ loading = new LoadingLayer(true),
+ channelListing = new ChannelListing
{
RelativeSizeAxes = Axes.Both,
},
},
},
- chatContainer = new Container
+ textBar = new ChatTextBar
{
- Name = @"chat container",
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- RelativeSizeAxes = Axes.Both,
- Height = DEFAULT_HEIGHT,
- Children = new[]
- {
- new Container
- {
- Name = @"chat area",
- RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Top = TAB_AREA_HEIGHT },
- Children = new Drawable[]
- {
- chatBackground = new Box
- {
- RelativeSizeAxes = Axes.Both,
- },
- new OnlineViewContainer("Sign in to chat")
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- currentChannelContainer = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding
- {
- Bottom = text_box_height
- },
- },
- new Container
- {
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- RelativeSizeAxes = Axes.X,
- Height = text_box_height,
- Padding = new MarginPadding
- {
- Top = padding * 2,
- Bottom = padding * 2,
- Left = ChatLine.LEFT_PADDING + padding * 2,
- Right = padding * 2,
- },
- Children = new Drawable[]
- {
- textBox = new FocusedTextBox
- {
- RelativeSizeAxes = Axes.Both,
- Height = 1,
- PlaceholderText = Resources.Localisation.Web.ChatStrings.InputPlaceholder,
- ReleaseFocusOnCommit = false,
- HoldFocus = true,
- }
- }
- },
- loading = new LoadingSpinner(),
- },
- }
- }
- },
- tabsArea = new TabsArea
- {
- Children = new Drawable[]
- {
- tabBackground = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.Black,
- },
- new Sprite
- {
- Texture = textures.Get(IconTexture),
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Size = new Vector2(OverlayTitle.ICON_SIZE),
- Margin = new MarginPadding { Left = 10 },
- },
- ChannelTabControl = CreateChannelTabControl().With(d =>
- {
- d.Anchor = Anchor.BottomLeft;
- d.Origin = Anchor.BottomLeft;
- d.RelativeSizeAxes = Axes.Both;
- d.OnRequestLeave = channelManager.LeaveChannel;
- d.IsSwitchable = true;
- }),
- }
- },
- },
+ RelativeSizeAxes = Axes.X,
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ Padding = new MarginPadding { Left = side_bar_width },
},
};
-
- availableChannels.BindTo(channelManager.AvailableChannels);
- joinedChannels.BindTo(channelManager.JoinedChannels);
- currentChannel.BindTo(channelManager.CurrentChannel);
-
- textBox.OnCommit += postMessage;
-
- ChannelTabControl.Current.ValueChanged += current => currentChannel.Value = current.NewValue;
- ChannelTabControl.ChannelSelectorActive.ValueChanged += active => ChannelSelectionOverlay.State.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden;
- ChannelSelectionOverlay.State.ValueChanged += state =>
- {
- // Propagate the visibility state to ChannelSelectorActive
- ChannelTabControl.ChannelSelectorActive.Value = state.NewValue == Visibility.Visible;
-
- if (state.NewValue == Visibility.Visible)
- {
- textBox.HoldFocus = false;
- if (1f - ChatHeight.Value < channel_selection_min_height)
- this.TransformBindableTo(ChatHeight, 1f - channel_selection_min_height, 800, Easing.OutQuint);
- }
- else
- textBox.HoldFocus = true;
- };
-
- ChannelSelectionOverlay.OnRequestJoin = channel => channelManager.JoinChannel(channel);
- ChannelSelectionOverlay.OnRequestLeave = channelManager.LeaveChannel;
-
- ChatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight);
- ChatHeight.BindValueChanged(height =>
- {
- chatContainer.Height = height.NewValue;
- channelSelectionContainer.Height = 1f - height.NewValue;
- tabBackground.FadeTo(height.NewValue == 1f ? 1f : 0.8f, 200);
- }, true);
-
- chatBackground.Colour = colours.ChatBlue;
-
- loading.Show();
-
- // This is a relatively expensive (and blocking) operation.
- // Scheduling it ensures that it won't be performed unless the user decides to open chat.
- // TODO: Refactor OsuFocusedOverlayContainer / OverlayContainer to support delayed content loading.
- Schedule(() =>
- {
- // TODO: consider scheduling bindable callbacks to not perform when overlay is not present.
- joinedChannels.BindCollectionChanged(joinedChannelsChanged, true);
- availableChannels.BindCollectionChanged(availableChannelsChanged, true);
- currentChannel.BindValueChanged(currentChannelChanged, true);
- });
}
- private void currentChannelChanged(ValueChangedEvent e)
+ protected override void LoadComplete()
{
- if (e.NewValue == null)
+ base.LoadComplete();
+
+ config.BindWith(OsuSetting.ChatDisplayHeight, chatHeight);
+
+ chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true);
+
+ currentChannel.BindTo(channelManager.CurrentChannel);
+ joinedChannels.BindTo(channelManager.JoinedChannels);
+ availableChannels.BindTo(channelManager.AvailableChannels);
+
+ Schedule(() =>
{
- textBox.Current.Disabled = true;
- currentChannelContainer.Clear(false);
- ChannelSelectionOverlay.Show();
- return;
- }
+ currentChannel.BindValueChanged(currentChannelChanged, true);
+ joinedChannels.BindCollectionChanged(joinedChannelsChanged, true);
+ availableChannels.BindCollectionChanged(availableChannelsChanged, true);
+ });
- if (e.NewValue is ChannelSelectorTabItem.ChannelSelectorTabChannel)
- return;
+ channelList.OnRequestSelect += channel => channelManager.CurrentChannel.Value = channel;
+ channelList.OnRequestLeave += channel => channelManager.LeaveChannel(channel);
- textBox.Current.Disabled = e.NewValue.ReadOnly;
+ channelListing.OnRequestJoin += channel => channelManager.JoinChannel(channel);
+ channelListing.OnRequestLeave += channel => channelManager.LeaveChannel(channel);
- if (ChannelTabControl.Current.Value != e.NewValue)
- Scheduler.Add(() => ChannelTabControl.Current.Value = e.NewValue);
-
- var loaded = loadedChannels.Find(d => d.Channel == e.NewValue);
-
- if (loaded == null)
- {
- currentChannelContainer.FadeOut(500, Easing.OutQuint);
- loading.Show();
-
- loaded = new DrawableChannel(e.NewValue);
- loadedChannels.Add(loaded);
- LoadComponentAsync(loaded, l =>
- {
- if (currentChannel.Value != e.NewValue)
- return;
-
- // check once more to ensure the channel hasn't since been removed from the loaded channels list (may have been left by some automated means).
- if (!loadedChannels.Contains(loaded))
- return;
-
- loading.Hide();
-
- currentChannelContainer.Clear(false);
- currentChannelContainer.Add(loaded);
- currentChannelContainer.FadeIn(500, Easing.OutQuint);
- });
- }
- else
- {
- currentChannelContainer.Clear(false);
- currentChannelContainer.Add(loaded);
- }
-
- // mark channel as read when channel switched
- if (e.NewValue.Messages.Any())
- channelManager.MarkChannelAsRead(e.NewValue);
+ textBar.OnSearchTermsChanged += searchTerms => channelListing.SearchTerm = searchTerms;
+ textBar.OnChatMessageCommitted += handleChatMessage;
}
///
@@ -320,7 +190,7 @@ namespace osu.Game.Overlays
if (!channel.Joined.Value)
channel = channelManager.JoinChannel(channel);
- currentChannel.Value = channel;
+ channelManager.CurrentChannel.Value = channel;
}
channel.HighlightedMessage.Value = message;
@@ -328,159 +198,172 @@ namespace osu.Game.Overlays
Show();
}
- private float startDragChatHeight;
- private bool isDragging;
-
- protected override bool OnDragStart(DragStartEvent e)
- {
- isDragging = tabsArea.IsHovered;
-
- if (!isDragging)
- return base.OnDragStart(e);
-
- startDragChatHeight = ChatHeight.Value;
- return true;
- }
-
- protected override void OnDrag(DragEvent e)
- {
- if (isDragging)
- {
- float targetChatHeight = startDragChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y;
-
- // If the channel selection screen is shown, mind its minimum height
- if (ChannelSelectionOverlay.State.Value == Visibility.Visible && targetChatHeight > 1f - channel_selection_min_height)
- targetChatHeight = 1f - channel_selection_min_height;
-
- ChatHeight.Value = targetChatHeight;
- }
- }
-
- protected override void OnDragEnd(DragEndEvent e)
- {
- isDragging = false;
- base.OnDragEnd(e);
- }
-
- private void selectTab(int index)
- {
- var channel = ChannelTabControl.Items
- .Where(tab => !(tab is ChannelSelectorTabItem.ChannelSelectorTabChannel))
- .ElementAtOrDefault(index);
- if (channel != null)
- ChannelTabControl.Current.Value = channel;
- }
-
- protected override bool OnKeyDown(KeyDownEvent e)
- {
- if (e.AltPressed)
- {
- switch (e.Key)
- {
- case Key.Number1:
- case Key.Number2:
- case Key.Number3:
- case Key.Number4:
- case Key.Number5:
- case Key.Number6:
- case Key.Number7:
- case Key.Number8:
- case Key.Number9:
- selectTab((int)e.Key - (int)Key.Number1);
- return true;
-
- case Key.Number0:
- selectTab(9);
- return true;
- }
- }
-
- return base.OnKeyDown(e);
- }
-
public bool OnPressed(KeyBindingPressEvent e)
{
switch (e.Action)
{
case PlatformAction.TabNew:
- ChannelTabControl.SelectChannelSelectorTab();
+ currentChannel.Value = channelList.ChannelListingChannel;
+ return true;
+
+ case PlatformAction.DocumentClose:
+ channelManager.LeaveChannel(currentChannel.Value);
return true;
case PlatformAction.TabRestore:
channelManager.JoinLastClosedChannel();
return true;
- case PlatformAction.DocumentClose:
- channelManager.LeaveChannel(currentChannel.Value);
+ case PlatformAction.DocumentPrevious:
+ cycleChannel(-1);
return true;
- }
- return false;
+ case PlatformAction.DocumentNext:
+ cycleChannel(1);
+ return true;
+
+ default:
+ return false;
+ }
}
public void OnReleased(KeyBindingReleaseEvent e)
{
}
- public override bool AcceptsFocus => true;
-
- protected override void OnFocus(FocusEvent e)
+ protected override bool OnDragStart(DragStartEvent e)
{
- // this is necessary as textbox is masked away and therefore can't get focus :(
- textBox.TakeFocus();
- base.OnFocus(e);
+ isDraggingTopBar = topBar.IsHovered;
+
+ if (!isDraggingTopBar)
+ return base.OnDragStart(e);
+
+ dragStartChatHeight = chatHeight.Value;
+ return true;
+ }
+
+ protected override void OnDrag(DragEvent e)
+ {
+ if (!isDraggingTopBar)
+ return;
+
+ float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y;
+ chatHeight.Value = targetChatHeight;
+ }
+
+ protected override void OnDragEnd(DragEndEvent e)
+ {
+ isDraggingTopBar = false;
+ base.OnDragEnd(e);
}
protected override void PopIn()
{
+ base.PopIn();
+
this.MoveToY(0, transition_length, Easing.OutQuint);
this.FadeIn(transition_length, Easing.OutQuint);
-
- textBox.HoldFocus = true;
-
- base.PopIn();
}
protected override void PopOut()
{
+ base.PopOut();
+
this.MoveToY(Height, transition_length, Easing.InSine);
this.FadeOut(transition_length, Easing.InSine);
- ChannelSelectionOverlay.Hide();
-
- textBox.HoldFocus = false;
- base.PopOut();
+ textBar.TextBoxKillFocus();
}
+ protected override void OnFocus(FocusEvent e)
+ {
+ textBar.TextBoxTakeFocus();
+ base.OnFocus(e);
+ }
+
+ private void currentChannelChanged(ValueChangedEvent channel)
+ {
+ Channel? newChannel = channel.NewValue;
+
+ // null channel denotes that we should be showing the listing.
+ if (newChannel == null)
+ {
+ currentChannel.Value = channelList.ChannelListingChannel;
+ return;
+ }
+
+ if (newChannel is ChannelListing.ChannelListingChannel)
+ {
+ currentChannelContainer.Clear(false);
+ channelListing.Show();
+ textBar.ShowSearch.Value = true;
+ }
+ else
+ {
+ channelListing.Hide();
+ textBar.ShowSearch.Value = false;
+
+ if (loadedChannels.ContainsKey(newChannel))
+ {
+ currentChannelContainer.Clear(false);
+ currentChannelContainer.Add(loadedChannels[newChannel]);
+ }
+ else
+ {
+ loading.Show();
+
+ // Ensure the drawable channel is stored before async load to prevent double loading
+ ChatOverlayDrawableChannel drawableChannel = CreateDrawableChannel(newChannel);
+ loadedChannels.Add(newChannel, drawableChannel);
+
+ LoadComponentAsync(drawableChannel, loadedDrawable =>
+ {
+ // Ensure the current channel hasn't changed by the time the load completes
+ if (currentChannel.Value != loadedDrawable.Channel)
+ return;
+
+ // Ensure the cached reference hasn't been removed from leaving the channel
+ if (!loadedChannels.ContainsKey(loadedDrawable.Channel))
+ return;
+
+ currentChannelContainer.Clear(false);
+ currentChannelContainer.Add(loadedDrawable);
+ loading.Hide();
+ });
+ }
+ }
+
+ // Mark channel as read when channel switched
+ if (newChannel.Messages.Any())
+ channelManager.MarkChannelAsRead(newChannel);
+ }
+
+ protected virtual ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) => new ChatOverlayDrawableChannel(newChannel);
+
private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
- foreach (Channel channel in args.NewItems.Cast())
- {
- if (channel.Type != ChannelType.Multiplayer)
- ChannelTabControl.AddChannel(channel);
- }
+ IEnumerable newChannels = args.NewItems.OfType().Where(isChatChannel);
+
+ foreach (var channel in newChannels)
+ channelList.AddChannel(channel);
break;
case NotifyCollectionChangedAction.Remove:
- foreach (Channel channel in args.OldItems.Cast())
+ IEnumerable leftChannels = args.OldItems.OfType().Where(isChatChannel);
+
+ foreach (var channel in leftChannels)
{
- if (!ChannelTabControl.Items.Contains(channel))
- continue;
+ channelList.RemoveChannel(channel);
- ChannelTabControl.RemoveChannel(channel);
-
- var loaded = loadedChannels.Find(c => c.Channel == channel);
-
- if (loaded != null)
+ if (loadedChannels.ContainsKey(channel))
{
- // Because the container is only cleared in the async load callback of a new channel, it is forcefully cleared
- // to ensure that the previous channel doesn't get updated after it's disposed
- loadedChannels.Remove(loaded);
- currentChannelContainer.Remove(loaded);
+ ChatOverlayDrawableChannel loaded = loadedChannels[channel];
+ loadedChannels.Remove(channel);
+ // DrawableChannel removed from cache must be manually disposed
loaded.Dispose();
}
}
@@ -490,35 +373,47 @@ namespace osu.Game.Overlays
}
private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
- {
- ChannelSelectionOverlay.UpdateAvailableChannels(availableChannels);
- }
+ => channelListing.UpdateAvailableChannels(channelManager.AvailableChannels);
- private void postMessage(TextBox textBox, bool newText)
+ private void handleChatMessage(string message)
{
- string text = textBox.Text.Trim();
-
- if (string.IsNullOrWhiteSpace(text))
+ if (string.IsNullOrWhiteSpace(message))
return;
- if (text[0] == '/')
- channelManager.PostCommand(text.Substring(1));
+ if (message[0] == '/')
+ channelManager.PostCommand(message.Substring(1));
else
- channelManager.PostMessage(text);
-
- textBox.Text = string.Empty;
+ channelManager.PostMessage(message);
}
- private class TabsArea : Container
+ private void cycleChannel(int direction)
{
- // IsHovered is used
- public override bool HandlePositionalInput => true;
+ List overlayChannels = channelList.Channels.ToList();
- public TabsArea()
+ if (overlayChannels.Count < 2)
+ return;
+
+ int currentIndex = overlayChannels.IndexOf(currentChannel.Value);
+
+ currentChannel.Value = overlayChannels[(currentIndex + direction + overlayChannels.Count) % overlayChannels.Count];
+
+ channelList.ScrollChannelIntoView(currentChannel.Value);
+ }
+
+ ///
+ /// Whether a channel should be displayed in this overlay, based on its type.
+ ///
+ private static bool isChatChannel(Channel channel)
+ {
+ switch (channel.Type)
{
- Name = @"tabs area";
- RelativeSizeAxes = Axes.X;
- Height = TAB_AREA_HEIGHT;
+ case ChannelType.Multiplayer:
+ case ChannelType.Spectator:
+ case ChannelType.Temporary:
+ return false;
+
+ default:
+ return true;
}
}
}
diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs
deleted file mode 100644
index ef479ea21b..0000000000
--- a/osu.Game/Overlays/ChatOverlayV2.cs
+++ /dev/null
@@ -1,350 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable enable
-
-using System.Collections;
-using System.Collections.Generic;
-using System.Collections.Specialized;
-using System.Diagnostics;
-using System.Linq;
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Input.Events;
-using osu.Framework.Localisation;
-using osu.Game.Configuration;
-using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Localisation;
-using osu.Game.Online.Chat;
-using osu.Game.Overlays.Chat;
-using osu.Game.Overlays.Chat.ChannelList;
-using osu.Game.Overlays.Chat.Listing;
-
-namespace osu.Game.Overlays
-{
- public class ChatOverlayV2 : OsuFocusedOverlayContainer, INamedOverlayComponent
- {
- public string IconTexture => "Icons/Hexacons/messaging";
- public LocalisableString Title => ChatStrings.HeaderTitle;
- public LocalisableString Description => ChatStrings.HeaderDescription;
-
- private ChatOverlayTopBar topBar = null!;
- private ChannelList channelList = null!;
- private LoadingLayer loading = null!;
- private ChannelListing channelListing = null!;
- private ChatTextBar textBar = null!;
- private Container currentChannelContainer = null!;
-
- private readonly Dictionary loadedChannels = new Dictionary();
-
- protected IEnumerable DrawableChannels => loadedChannels.Values;
-
- private readonly BindableFloat chatHeight = new BindableFloat();
- private bool isDraggingTopBar;
- private float dragStartChatHeight;
-
- private const int transition_length = 500;
- private const float default_chat_height = 0.4f;
- private const float top_bar_height = 40;
- private const float side_bar_width = 190;
- private const float chat_bar_height = 60;
-
- [Resolved]
- private OsuConfigManager config { get; set; } = null!;
-
- [Resolved]
- private ChannelManager channelManager { get; set; } = null!;
-
- [Cached]
- private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
-
- [Cached]
- private readonly Bindable currentChannel = new Bindable();
-
- private readonly IBindableList availableChannels = new BindableList();
- private readonly IBindableList joinedChannels = new BindableList();
-
- public ChatOverlayV2()
- {
- Height = default_chat_height;
-
- Masking = true;
-
- const float corner_radius = 7f;
-
- CornerRadius = corner_radius;
-
- // Hack to hide the bottom edge corner radius off-screen.
- Margin = new MarginPadding { Bottom = -corner_radius };
- Padding = new MarginPadding { Bottom = corner_radius };
-
- Anchor = Anchor.BottomCentre;
- Origin = Anchor.BottomCentre;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- // Required for the pop in/out animation
- RelativePositionAxes = Axes.Both;
-
- Children = new Drawable[]
- {
- topBar = new ChatOverlayTopBar
- {
- RelativeSizeAxes = Axes.X,
- Height = top_bar_height,
- },
- channelList = new ChannelList
- {
- RelativeSizeAxes = Axes.Y,
- Width = side_bar_width,
- Padding = new MarginPadding { Top = top_bar_height },
- },
- new Container
- {
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- Padding = new MarginPadding
- {
- Top = top_bar_height,
- Left = side_bar_width,
- Bottom = chat_bar_height,
- },
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = colourProvider.Background4,
- },
- currentChannelContainer = new Container
- {
- RelativeSizeAxes = Axes.Both,
- },
- loading = new LoadingLayer(true),
- channelListing = new ChannelListing
- {
- RelativeSizeAxes = Axes.Both,
- },
- },
- },
- textBar = new ChatTextBar
- {
- RelativeSizeAxes = Axes.X,
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- Padding = new MarginPadding { Left = side_bar_width },
- },
- };
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- config.BindWith(OsuSetting.ChatDisplayHeight, chatHeight);
-
- chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true);
-
- currentChannel.BindTo(channelManager.CurrentChannel);
- currentChannel.BindValueChanged(currentChannelChanged, true);
-
- joinedChannels.BindTo(channelManager.JoinedChannels);
- joinedChannels.BindCollectionChanged(joinedChannelsChanged, true);
-
- availableChannels.BindTo(channelManager.AvailableChannels);
- availableChannels.BindCollectionChanged(availableChannelsChanged, true);
-
- channelList.OnRequestSelect += channel => channelManager.CurrentChannel.Value = channel;
- channelList.OnRequestLeave += channel => channelManager.LeaveChannel(channel);
-
- channelListing.OnRequestJoin += channel => channelManager.JoinChannel(channel);
- channelListing.OnRequestLeave += channel => channelManager.LeaveChannel(channel);
-
- textBar.OnSearchTermsChanged += searchTerms => channelListing.SearchTerm = searchTerms;
- textBar.OnChatMessageCommitted += handleChatMessage;
- }
-
- ///
- /// Highlights a certain message in the specified channel.
- ///
- /// The message to highlight.
- /// The channel containing the message.
- public void HighlightMessage(Message message, Channel channel)
- {
- Debug.Assert(channel.Id == message.ChannelId);
-
- if (currentChannel.Value?.Id != channel.Id)
- {
- if (!channel.Joined.Value)
- channel = channelManager.JoinChannel(channel);
-
- channelManager.CurrentChannel.Value = channel;
- }
-
- channel.HighlightedMessage.Value = message;
-
- Show();
- }
-
- protected override bool OnDragStart(DragStartEvent e)
- {
- isDraggingTopBar = topBar.IsHovered;
-
- if (!isDraggingTopBar)
- return base.OnDragStart(e);
-
- dragStartChatHeight = chatHeight.Value;
- return true;
- }
-
- protected override void OnDrag(DragEvent e)
- {
- if (!isDraggingTopBar)
- return;
-
- float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y;
- chatHeight.Value = targetChatHeight;
- }
-
- protected override void OnDragEnd(DragEndEvent e)
- {
- isDraggingTopBar = false;
- base.OnDragEnd(e);
- }
-
- protected override void PopIn()
- {
- base.PopIn();
-
- this.MoveToY(0, transition_length, Easing.OutQuint);
- this.FadeIn(transition_length, Easing.OutQuint);
- }
-
- protected override void PopOut()
- {
- base.PopOut();
-
- this.MoveToY(Height, transition_length, Easing.InSine);
- this.FadeOut(transition_length, Easing.InSine);
-
- textBar.TextBoxKillFocus();
- }
-
- protected override void OnFocus(FocusEvent e)
- {
- textBar.TextBoxTakeFocus();
- base.OnFocus(e);
- }
-
- private void currentChannelChanged(ValueChangedEvent channel)
- {
- Channel? newChannel = channel.NewValue;
-
- // null channel denotes that we should be showing the listing.
- if (newChannel == null)
- {
- currentChannel.Value = channelList.ChannelListingChannel;
- return;
- }
-
- if (newChannel is ChannelListing.ChannelListingChannel)
- {
- currentChannelContainer.Clear(false);
- channelListing.Show();
- textBar.ShowSearch.Value = true;
- }
- else
- {
- channelListing.Hide();
- textBar.ShowSearch.Value = false;
-
- if (loadedChannels.ContainsKey(newChannel))
- {
- currentChannelContainer.Clear(false);
- currentChannelContainer.Add(loadedChannels[newChannel]);
- }
- else
- {
- loading.Show();
-
- // Ensure the drawable channel is stored before async load to prevent double loading
- ChatOverlayDrawableChannel drawableChannel = CreateDrawableChannel(newChannel);
- loadedChannels.Add(newChannel, drawableChannel);
-
- LoadComponentAsync(drawableChannel, loadedDrawable =>
- {
- // Ensure the current channel hasn't changed by the time the load completes
- if (currentChannel.Value != loadedDrawable.Channel)
- return;
-
- // Ensure the cached reference hasn't been removed from leaving the channel
- if (!loadedChannels.ContainsKey(loadedDrawable.Channel))
- return;
-
- currentChannelContainer.Clear(false);
- currentChannelContainer.Add(loadedDrawable);
- loading.Hide();
- });
- }
- }
- }
-
- protected virtual ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) => new ChatOverlayDrawableChannel(newChannel);
-
- private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
- {
- switch (args.Action)
- {
- case NotifyCollectionChangedAction.Add:
- IEnumerable newChannels = filterChannels(args.NewItems);
-
- foreach (var channel in newChannels)
- channelList.AddChannel(channel);
-
- break;
-
- case NotifyCollectionChangedAction.Remove:
- IEnumerable leftChannels = filterChannels(args.OldItems);
-
- foreach (var channel in leftChannels)
- {
- channelList.RemoveChannel(channel);
-
- if (loadedChannels.ContainsKey(channel))
- {
- ChatOverlayDrawableChannel loaded = loadedChannels[channel];
- loadedChannels.Remove(channel);
- // DrawableChannel removed from cache must be manually disposed
- loaded.Dispose();
- }
- }
-
- break;
- }
- }
-
- private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
- => channelListing.UpdateAvailableChannels(channelManager.AvailableChannels);
-
- private IEnumerable filterChannels(IList channels)
- => channels.Cast().Where(c => c.Type == ChannelType.Public || c.Type == ChannelType.PM);
-
- private void handleChatMessage(string message)
- {
- if (string.IsNullOrWhiteSpace(message))
- return;
-
- if (message[0] == '/')
- channelManager.PostCommand(message.Substring(1));
- else
- channelManager.PostMessage(message);
- }
- }
-}
diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs
index a9312e9a3a..23f67a06cb 100644
--- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs
+++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.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.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
@@ -9,11 +10,16 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Game.Database;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Spectator;
+using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.Play;
@@ -24,26 +30,62 @@ namespace osu.Game.Overlays.Dashboard
{
internal class CurrentlyPlayingDisplay : CompositeDrawable
{
+ private const float search_textbox_height = 40;
+ private const float padding = 10;
+
private readonly IBindableList playingUsers = new BindableList();
- private FillFlowContainer userFlow;
+ private SearchContainer userFlow;
+ private BasicSearchTextBox searchTextBox;
[Resolved]
private SpectatorClient spectatorClient { get; set; }
[BackgroundDependencyLoader]
- private void load()
+ private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
- InternalChild = userFlow = new FillFlowContainer
+ InternalChildren = new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding(10),
- Spacing = new Vector2(10),
+ new Box
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = padding * 2 + search_textbox_height,
+ Colour = colourProvider.Background4,
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Padding = new MarginPadding(padding),
+ Child = searchTextBox = new BasicSearchTextBox
+ {
+ RelativeSizeAxes = Axes.X,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Height = search_textbox_height,
+ ReleaseFocusOnCommit = false,
+ HoldFocus = true,
+ PlaceholderText = HomeStrings.SearchPlaceholder,
+ },
+ },
+ userFlow = new SearchContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(10),
+ Padding = new MarginPadding
+ {
+ Top = padding * 3 + search_textbox_height,
+ Bottom = padding,
+ Right = padding,
+ Left = padding,
+ },
+ },
};
+
+ searchTextBox.Current.ValueChanged += text => userFlow.SearchTerm = text.NewValue;
}
[Resolved]
@@ -57,6 +99,13 @@ namespace osu.Game.Overlays.Dashboard
playingUsers.BindCollectionChanged(onPlayingUsersChanged, true);
}
+ protected override void OnFocus(FocusEvent e)
+ {
+ base.OnFocus(e);
+
+ searchTextBox.TakeFocus();
+ }
+
private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() =>
{
switch (e.Action)
@@ -102,17 +151,34 @@ namespace osu.Game.Overlays.Dashboard
panel.Origin = Anchor.TopCentre;
});
- private class PlayingUserPanel : CompositeDrawable
+ public class PlayingUserPanel : CompositeDrawable, IFilterable
{
public readonly APIUser User;
+ public IEnumerable FilterTerms { get; }
+
[Resolved(canBeNull: true)]
private IPerformFromScreenRunner performer { get; set; }
+ public bool FilteringActive { set; get; }
+
+ public bool MatchingFilter
+ {
+ set
+ {
+ if (value)
+ Show();
+ else
+ Hide();
+ }
+ }
+
public PlayingUserPanel(APIUser user)
{
User = user;
+ FilterTerms = new LocalisableString[] { User.Username };
+
AutoSizeAxes = Axes.Both;
}
diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs
index 83ad8faf1c..79d972bdcc 100644
--- a/osu.Game/Overlays/DashboardOverlay.cs
+++ b/osu.Game/Overlays/DashboardOverlay.cs
@@ -16,6 +16,8 @@ namespace osu.Game.Overlays
protected override DashboardOverlayHeader CreateHeader() => new DashboardOverlayHeader();
+ public override bool AcceptsFocus => false;
+
protected override void CreateDisplayToLoad(DashboardOverlayTabs tab)
{
switch (tab)
diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs
index 17e04c0c99..ddcee7c040 100644
--- a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs
+++ b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs
@@ -154,12 +154,15 @@ namespace osu.Game.Overlays.FirstRunSetup
var downloadTracker = tutorialDownloader.DownloadTrackers.First();
+ downloadTracker.State.BindValueChanged(state =>
+ {
+ if (state.NewValue == DownloadState.LocallyAvailable)
+ downloadTutorialButton.Complete();
+ }, true);
+
downloadTracker.Progress.BindValueChanged(progress =>
{
downloadTutorialButton.SetProgress(progress.NewValue, false);
-
- if (progress.NewValue == 1)
- downloadTutorialButton.Complete();
}, true);
}
diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs
index 7b0de4affe..a5bece0832 100644
--- a/osu.Game/Overlays/FirstRunSetupOverlay.cs
+++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs
@@ -76,10 +76,10 @@ namespace osu.Game.Overlays
private void load(OsuColour colours, LegacyImportManager? legacyImportManager)
{
steps.Add(typeof(ScreenWelcome));
+ steps.Add(typeof(ScreenUIScale));
steps.Add(typeof(ScreenBeatmaps));
if (legacyImportManager?.SupportsImportFromStable == true)
steps.Add(typeof(ScreenImportFromStable));
- steps.Add(typeof(ScreenUIScale));
steps.Add(typeof(ScreenBehaviour));
Header.Title = FirstRunSetupOverlayStrings.FirstRunSetupTitle;
diff --git a/osu.Game/Overlays/Mods/DeselectAllModsButton.cs b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs
new file mode 100644
index 0000000000..8288d34c95
--- /dev/null
+++ b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs
@@ -0,0 +1,54 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Bindables;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Input.Bindings;
+using osu.Game.Localisation;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Overlays.Mods
+{
+ public class DeselectAllModsButton : ShearedButton, IKeyBindingHandler
+ {
+ private readonly Bindable> selectedMods = new Bindable>();
+
+ public DeselectAllModsButton(ModSelectOverlay modSelectOverlay)
+ : base(ModSelectOverlay.BUTTON_WIDTH)
+ {
+ Text = CommonStrings.DeselectAll;
+ Action = modSelectOverlay.DeselectAll;
+
+ selectedMods.BindTo(modSelectOverlay.SelectedMods);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ selectedMods.BindValueChanged(_ => updateEnabledState(), true);
+ }
+
+ private void updateEnabledState()
+ {
+ Enabled.Value = selectedMods.Value.Any();
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ if (e.Repeat || e.Action != GlobalAction.DeselectAllMods)
+ return false;
+
+ TriggerClick();
+ return true;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs
index 9bb3f8bd9e..42f9daec4d 100644
--- a/osu.Game/Overlays/Mods/ModColumn.cs
+++ b/osu.Game/Overlays/Mods/ModColumn.cs
@@ -267,7 +267,7 @@ namespace osu.Game.Overlays.Mods
{
cancellationTokenSource?.Cancel();
- var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0)));
+ var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = Vector2.Zero));
Task? loadTask;
diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs
index 7010342bd8..358bdd3202 100644
--- a/osu.Game/Overlays/Mods/ModPanel.cs
+++ b/osu.Game/Overlays/Mods/ModPanel.cs
@@ -70,7 +70,7 @@ namespace osu.Game.Overlays.Mods
Content.Masking = true;
Content.CornerRadius = CORNER_RADIUS;
Content.BorderThickness = 2;
- Content.Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0);
+ Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0);
Children = new Drawable[]
{
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index f1a998bd3c..4bad34d94f 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -29,11 +29,20 @@ namespace osu.Game.Overlays.Mods
{
public abstract class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler
{
- protected const int BUTTON_WIDTH = 200;
+ public const int BUTTON_WIDTH = 200;
[Cached]
public Bindable> SelectedMods { get; private set; } = new Bindable>(Array.Empty());
+ ///
+ /// Contains a dictionary with the current of all mods applicable for the current ruleset.
+ ///
+ ///
+ /// Contrary to and , the instances
+ /// inside the objects are owned solely by this instance.
+ ///
+ public Bindable>> AvailableMods { get; } = new Bindable>>(new Dictionary>());
+
private Func isValidMod = m => true;
///
@@ -76,16 +85,12 @@ namespace osu.Game.Overlays.Mods
};
}
- yield return deselectAllButton = new ShearedButton(BUTTON_WIDTH)
- {
- Text = CommonStrings.DeselectAll,
- Action = DeselectAll
- };
+ yield return new DeselectAllModsButton(this);
}
- private readonly Bindable>> availableMods = new Bindable>>();
- private readonly Dictionary> localAvailableMods = new Dictionary>();
- private IEnumerable allLocalAvailableMods => localAvailableMods.SelectMany(pair => pair.Value);
+ private readonly Bindable>> globalAvailableMods = new Bindable>>();
+
+ private IEnumerable allAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value);
private readonly BindableBool customisationVisible = new BindableBool();
@@ -98,7 +103,6 @@ namespace osu.Game.Overlays.Mods
private DifficultyMultiplierDisplay? multiplierDisplay;
private ShearedToggleButton? customisationButton;
- private ShearedButton? deselectAllButton;
protected ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green)
: base(colourScheme)
@@ -209,13 +213,13 @@ namespace osu.Game.Overlays.Mods
})
};
- availableMods.BindTo(game.AvailableMods);
+ globalAvailableMods.BindTo(game.AvailableMods);
}
protected override void LoadComplete()
{
// this is called before base call so that the mod state is populated early, and the transition in `PopIn()` can play out properly.
- availableMods.BindValueChanged(_ => createLocalMods(), true);
+ globalAvailableMods.BindValueChanged(_ => createLocalMods(), true);
base.LoadComplete();
@@ -247,7 +251,7 @@ namespace osu.Game.Overlays.Mods
///
/// Select all visible mods in all columns.
///
- protected void SelectAll()
+ public void SelectAll()
{
foreach (var column in columnFlow.Columns)
column.SelectAll();
@@ -256,7 +260,7 @@ namespace osu.Game.Overlays.Mods
///
/// Deselect all visible mods in all columns.
///
- protected void DeselectAll()
+ public void DeselectAll()
{
foreach (var column in columnFlow.Columns)
column.DeselectAll();
@@ -280,9 +284,9 @@ namespace osu.Game.Overlays.Mods
private void createLocalMods()
{
- localAvailableMods.Clear();
+ var newLocalAvailableMods = new Dictionary>();
- foreach (var (modType, mods) in availableMods.Value)
+ foreach (var (modType, mods) in globalAvailableMods.Value)
{
var modStates = mods.SelectMany(ModUtils.FlattenMod)
.Select(mod => new ModState(mod.DeepClone()))
@@ -291,18 +295,19 @@ namespace osu.Game.Overlays.Mods
foreach (var modState in modStates)
modState.Active.BindValueChanged(_ => updateFromInternalSelection());
- localAvailableMods[modType] = modStates;
+ newLocalAvailableMods[modType] = modStates;
}
+ AvailableMods.Value = newLocalAvailableMods;
filterMods();
foreach (var column in columnFlow.Columns)
- column.AvailableMods = localAvailableMods.GetValueOrDefault(column.ModType, Array.Empty());
+ column.AvailableMods = AvailableMods.Value.GetValueOrDefault(column.ModType, Array.Empty());
}
private void filterMods()
{
- foreach (var modState in allLocalAvailableMods)
+ foreach (var modState in allAvailableMods)
modState.Filtered.Value = !modState.Mod.HasImplementation || !IsValidMod.Invoke(modState.Mod);
}
@@ -383,7 +388,7 @@ namespace osu.Game.Overlays.Mods
var newSelection = new List();
- foreach (var modState in allLocalAvailableMods)
+ foreach (var modState in allAvailableMods)
{
var matchingSelectedMod = SelectedMods.Value.SingleOrDefault(selected => selected.GetType() == modState.Mod.GetType());
@@ -410,9 +415,9 @@ namespace osu.Game.Overlays.Mods
if (externalSelectionUpdateInProgress)
return;
- var candidateSelection = allLocalAvailableMods.Where(modState => modState.Active.Value)
- .Select(modState => modState.Mod)
- .ToArray();
+ var candidateSelection = allAvailableMods.Where(modState => modState.Active.Value)
+ .Select(modState => modState.Mod)
+ .ToArray();
SelectedMods.Value = ComputeNewModsFromSelection(SelectedMods.Value, candidateSelection);
}
@@ -514,10 +519,6 @@ namespace osu.Game.Overlays.Mods
hideOverlay(true);
return true;
}
-
- case GlobalAction.DeselectAllMods:
- deselectAllButton?.TriggerClick();
- return true;
}
return base.OnPressed(e);
diff --git a/osu.Game/Overlays/Mods/SelectAllModsButton.cs b/osu.Game/Overlays/Mods/SelectAllModsButton.cs
new file mode 100644
index 0000000000..f7078b2fa5
--- /dev/null
+++ b/osu.Game/Overlays/Mods/SelectAllModsButton.cs
@@ -0,0 +1,61 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Bindables;
+using osu.Framework.Input;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Localisation;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Screens.OnlinePlay;
+
+namespace osu.Game.Overlays.Mods
+{
+ public class SelectAllModsButton : ShearedButton, IKeyBindingHandler
+ {
+ private readonly Bindable> selectedMods = new Bindable>();
+ private readonly Bindable>> availableMods = new Bindable>>();
+
+ public SelectAllModsButton(FreeModSelectOverlay modSelectOverlay)
+ : base(ModSelectOverlay.BUTTON_WIDTH)
+ {
+ Text = CommonStrings.SelectAll;
+ Action = modSelectOverlay.SelectAll;
+
+ selectedMods.BindTo(modSelectOverlay.SelectedMods);
+ availableMods.BindTo(modSelectOverlay.AvailableMods);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ selectedMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState));
+ availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState));
+ updateEnabledState();
+ }
+
+ private void updateEnabledState()
+ {
+ Enabled.Value = availableMods.Value
+ .SelectMany(pair => pair.Value)
+ .Any(modState => !modState.Active.Value && !modState.Filtered.Value);
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ if (e.Repeat || e.Action != PlatformAction.SelectAll)
+ return false;
+
+ TriggerClick();
+ return true;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+ }
+}
diff --git a/osu.Game/Overlays/News/NewsCard.cs b/osu.Game/Overlays/News/NewsCard.cs
index 68d0704825..5ce0b9df9c 100644
--- a/osu.Game/Overlays/News/NewsCard.cs
+++ b/osu.Game/Overlays/News/NewsCard.cs
@@ -14,7 +14,6 @@ using osu.Framework.Platform;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.News
@@ -29,7 +28,6 @@ namespace osu.Game.Overlays.News
private TextFlowContainer main;
public NewsCard(APINewsPost post)
- : base(HoverSampleSet.Submit)
{
this.post = post;
diff --git a/osu.Game/Overlays/News/Sidebar/MonthSection.cs b/osu.Game/Overlays/News/Sidebar/MonthSection.cs
index aa83f89689..e2ce25660e 100644
--- a/osu.Game/Overlays/News/Sidebar/MonthSection.cs
+++ b/osu.Game/Overlays/News/Sidebar/MonthSection.cs
@@ -18,7 +18,6 @@ using System.Diagnostics;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Platform;
-using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.News.Sidebar
{
@@ -129,7 +128,6 @@ namespace osu.Game.Overlays.News.Sidebar
private readonly APINewsPost post;
public PostButton(APINewsPost post)
- : base(HoverSampleSet.Submit)
{
this.post = post;
diff --git a/osu.Game/Overlays/OverlayColourProvider.cs b/osu.Game/Overlays/OverlayColourProvider.cs
index 7bddb924a0..a4f6527024 100644
--- a/osu.Game/Overlays/OverlayColourProvider.cs
+++ b/osu.Game/Overlays/OverlayColourProvider.cs
@@ -72,6 +72,9 @@ namespace osu.Game.Overlays
case OverlayColourScheme.Green:
return 125 / 360f;
+ case OverlayColourScheme.Aquamarine:
+ return 160 / 360f;
+
case OverlayColourScheme.Purple:
return 255 / 360f;
@@ -94,5 +97,6 @@ namespace osu.Game.Overlays
Purple,
Blue,
Plum,
+ Aquamarine
}
}
diff --git a/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs b/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs
index b4a5d5e31b..4cfdf5cc86 100644
--- a/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
private Sample sampleOpen;
private Sample sampleClose;
- protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();
+ protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds();
public ExpandDetailsButton()
{
diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs b/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs
index 94ef5e5d86..13465f3bf8 100644
--- a/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs
@@ -6,7 +6,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Profile.Sections
{
@@ -18,7 +17,6 @@ namespace osu.Game.Overlays.Profile.Sections
private readonly IBeatmapInfo beatmapInfo;
protected BeatmapMetadataContainer(IBeatmapInfo beatmapInfo)
- : base(HoverSampleSet.Submit)
{
this.beatmapInfo = beatmapInfo;
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
index 602ace6dea..05890ad882 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
@@ -4,6 +4,7 @@
using System;
using System.Drawing;
using System.Linq;
+using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
@@ -13,6 +14,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Framework.Platform;
+using osu.Framework.Platform.Windows;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
@@ -34,10 +36,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private Bindable sizeFullscreen;
private readonly BindableList resolutions = new BindableList(new[] { new Size(9999, 9999) });
+ private readonly IBindable fullscreenCapability = new Bindable(FullscreenCapability.Capable);
[Resolved]
private OsuGameBase game { get; set; }
+ [Resolved]
+ private GameHost host { get; set; }
+
private SettingsDropdown resolutionDropdown;
private SettingsDropdown displayDropdown;
private SettingsDropdown windowModeDropdown;
@@ -65,6 +71,9 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
windowModes.BindTo(host.Window.SupportedWindowModes);
}
+ if (host.Window is WindowsWindow windowsWindow)
+ fullscreenCapability.BindTo(windowsWindow.FullscreenCapability);
+
Children = new Drawable[]
{
windowModeDropdown = new SettingsDropdown
@@ -139,6 +148,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}
},
};
+
+ fullscreenCapability.BindValueChanged(_ => Schedule(updateScreenModeWarning), true);
}
protected override void LoadComplete()
@@ -150,8 +161,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
windowModeDropdown.Current.BindValueChanged(mode =>
{
updateDisplayModeDropdowns();
-
- windowModeDropdown.WarningText = mode.NewValue != WindowMode.Fullscreen ? GraphicsSettingsStrings.NotFullscreenNote : default;
+ updateScreenModeWarning();
}, true);
windowModes.BindCollectionChanged((sender, args) =>
@@ -213,6 +223,48 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}
}
+ private void updateScreenModeWarning()
+ {
+ if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS)
+ {
+ if (windowModeDropdown.Current.Value == WindowMode.Fullscreen)
+ windowModeDropdown.SetNoticeText(LayoutSettingsStrings.FullscreenMacOSNote, true);
+ else
+ windowModeDropdown.ClearNoticeText();
+
+ return;
+ }
+
+ if (windowModeDropdown.Current.Value != WindowMode.Fullscreen)
+ {
+ windowModeDropdown.SetNoticeText(GraphicsSettingsStrings.NotFullscreenNote, true);
+ return;
+ }
+
+ if (host.Window is WindowsWindow)
+ {
+ switch (fullscreenCapability.Value)
+ {
+ case FullscreenCapability.Unknown:
+ windowModeDropdown.SetNoticeText(LayoutSettingsStrings.CheckingForFullscreenCapabilities, true);
+ break;
+
+ case FullscreenCapability.Capable:
+ windowModeDropdown.SetNoticeText(LayoutSettingsStrings.OsuIsRunningExclusiveFullscreen);
+ break;
+
+ case FullscreenCapability.Incapable:
+ windowModeDropdown.SetNoticeText(LayoutSettingsStrings.UnableToRunExclusiveFullscreen, true);
+ break;
+ }
+ }
+ else
+ {
+ // We can only detect exclusive fullscreen status on windows currently.
+ windowModeDropdown.ClearNoticeText();
+ }
+ }
+
private void bindPreviewEvent(Bindable bindable)
{
bindable.ValueChanged += _ =>
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs
index 653f30a018..8c3e45cd62 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs
@@ -48,7 +48,16 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
frameLimiterDropdown.Current.BindValueChanged(limit =>
{
- frameLimiterDropdown.WarningText = limit.NewValue == FrameSync.Unlimited ? GraphicsSettingsStrings.UnlimitedFramesNote : default;
+ switch (limit.NewValue)
+ {
+ case FrameSync.Unlimited:
+ frameLimiterDropdown.SetNoticeText(GraphicsSettingsStrings.UnlimitedFramesNote, true);
+ break;
+
+ default:
+ frameLimiterDropdown.ClearNoticeText();
+ break;
+ }
}, true);
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
index 4235dc0a05..1511d53b6b 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
@@ -117,9 +117,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
{
if (highPrecision.NewValue)
- highPrecisionMouse.WarningText = MouseSettingsStrings.HighPrecisionPlatformWarning;
+ highPrecisionMouse.SetNoticeText(MouseSettingsStrings.HighPrecisionPlatformWarning, true);
else
- highPrecisionMouse.WarningText = null;
+ highPrecisionMouse.ClearNoticeText();
}
}, true);
}
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 802d442ced..5d31c38ae7 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -11,6 +11,7 @@ using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK;
using osu.Game.Localisation;
@@ -95,11 +96,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input
Origin = Anchor.TopCentre,
Text = TabletSettingsStrings.NoTabletDetected,
},
- new SettingsNoticeText(colours)
+ new LinkFlowContainer(cp => cp.Colour = colours.Yellow)
{
TextAnchor = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
}.With(t =>
{
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows || RuntimeInfo.OS == RuntimeInfo.Platform.Linux)
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
index be4b0decd9..054de8dbd7 100644
--- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs
@@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
private SettingsButton deleteSkinsButton;
private SettingsButton restoreButton;
private SettingsButton undeleteButton;
+ private SettingsButton deleteBeatmapVideosButton;
[BackgroundDependencyLoader(permitNulls: true)]
private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, [CanBeNull] LegacyImportManager legacyImportManager, IDialogOverlay dialogOverlay)
@@ -58,6 +59,19 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
}
});
+ Add(deleteBeatmapVideosButton = new DangerousSettingsButton
+ {
+ Text = MaintenanceSettingsStrings.DeleteAllBeatmapVideos,
+ Action = () =>
+ {
+ dialogOverlay?.Push(new MassVideoDeleteConfirmationDialog(() =>
+ {
+ deleteBeatmapVideosButton.Enabled.Value = false;
+ Task.Run(beatmaps.DeleteAllVideos).ContinueWith(t => Schedule(() => deleteBeatmapVideosButton.Enabled.Value = true));
+ }));
+ }
+ });
+
if (legacyImportManager?.SupportsImportFromStable == true)
{
Add(importScoresButton = new SettingsButton
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MassVideoDeleteConfirmationDialog.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MassVideoDeleteConfirmationDialog.cs
new file mode 100644
index 0000000000..fc8c9d497b
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MassVideoDeleteConfirmationDialog.cs
@@ -0,0 +1,16 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+
+namespace osu.Game.Overlays.Settings.Sections.Maintenance
+{
+ public class MassVideoDeleteConfirmationDialog : MassDeleteConfirmationDialog
+ {
+ public MassVideoDeleteConfirmationDialog(Action deleteAction)
+ : base(deleteAction)
+ {
+ BodyText = "All beatmap videos? This cannot be undone!";
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index a34776ddf0..a87e65b735 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -127,9 +127,12 @@ namespace osu.Game.Overlays.Settings.Sections
dropdownItems.Add(skin.ToLive(realm));
dropdownItems.Insert(protectedCount, random_skin_info);
- skinDropdown.Items = dropdownItems;
+ Schedule(() =>
+ {
+ skinDropdown.Items = dropdownItems;
- updateSelectedSkinFromConfig();
+ updateSelectedSkinFromConfig();
+ });
}
private void updateSelectedSkinFromConfig()
diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs
index 284e9cb2de..fceffa09c5 100644
--- a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs
@@ -61,7 +61,10 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
user.BindValueChanged(u =>
{
- backgroundSourceDropdown.WarningText = u.NewValue?.IsSupporter != true ? UserInterfaceStrings.NotSupporterNote : default;
+ if (u.NewValue?.IsSupporter != true)
+ backgroundSourceDropdown.SetNoticeText(UserInterfaceStrings.NotSupporterNote, true);
+ else
+ backgroundSourceDropdown.ClearNoticeText();
}, true);
}
}
diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs
index 10aea92b22..9e4dc763ec 100644
--- a/osu.Game/Overlays/Settings/SettingsButton.cs
+++ b/osu.Game/Overlays/Settings/SettingsButton.cs
@@ -3,9 +3,12 @@
using System.Collections.Generic;
using System.Linq;
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
+using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Overlays.Settings
@@ -18,6 +21,12 @@ namespace osu.Game.Overlays.Settings
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS };
}
+ [BackgroundDependencyLoader(true)]
+ private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour colours)
+ {
+ DefaultBackgroundColour = overlayColourProvider?.Highlight1 ?? colours.Blue3;
+ }
+
public LocalisableString TooltipText { get; set; }
public override IEnumerable FilterTerms
diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs
index ee9daa1c0d..ea076b77ac 100644
--- a/osu.Game/Overlays/Settings/SettingsItem.cs
+++ b/osu.Game/Overlays/Settings/SettingsItem.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Settings
private SpriteText labelText;
- private OsuTextFlowContainer warningText;
+ private OsuTextFlowContainer noticeText;
public bool ShowsDefaultIndicator = true;
private readonly Container defaultValueIndicatorContainer;
@@ -70,27 +70,32 @@ namespace osu.Game.Overlays.Settings
}
///
- /// Text to be displayed at the bottom of this .
- /// Generally used to recommend the user change their setting as the current one is considered sub-optimal.
+ /// Clear any warning text.
///
- public LocalisableString? WarningText
+ public void ClearNoticeText()
{
- set
+ noticeText?.Expire();
+ noticeText = null;
+ }
+
+ ///
+ /// Set the text to be displayed at the bottom of this .
+ /// Generally used to provide feedback to a user about a sub-optimal setting.
+ ///
+ /// The text to display.
+ /// Whether the text is in a warning state. Will decide how this is visually represented.
+ public void SetNoticeText(LocalisableString text, bool isWarning = false)
+ {
+ ClearNoticeText();
+
+ // construct lazily for cases where the label is not needed (may be provided by the Control).
+ FlowContent.Add(noticeText = new LinkFlowContainer(cp => cp.Colour = isWarning ? colours.Yellow : colours.Green)
{
- bool hasValue = value != default;
-
- if (warningText == null)
- {
- if (!hasValue)
- return;
-
- // construct lazily for cases where the label is not needed (may be provided by the Control).
- FlowContent.Add(warningText = new SettingsNoticeText(colours) { Margin = new MarginPadding { Bottom = 5 } });
- }
-
- warningText.Alpha = hasValue ? 1 : 0;
- warningText.Text = value ?? default;
- }
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Margin = new MarginPadding { Bottom = 5 },
+ Text = text,
+ });
}
public virtual Bindable Current
diff --git a/osu.Game/Overlays/Settings/SettingsNoticeText.cs b/osu.Game/Overlays/Settings/SettingsNoticeText.cs
deleted file mode 100644
index 76ecf7edd4..0000000000
--- a/osu.Game/Overlays/Settings/SettingsNoticeText.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.
-
-using osu.Framework.Graphics;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Containers;
-
-namespace osu.Game.Overlays.Settings
-{
- public class SettingsNoticeText : LinkFlowContainer
- {
- public SettingsNoticeText(OsuColour colours)
- : base(s => s.Colour = colours.Yellow)
- {
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
- }
- }
-}
diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
index 4a839b048c..b686f11c13 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
@@ -20,7 +20,6 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Graphics;
@@ -83,7 +82,6 @@ namespace osu.Game.Overlays.Toolbar
private RealmAccess realm { get; set; }
protected ToolbarButton()
- : base(HoverSampleSet.Toolbar)
{
Width = Toolbar.HEIGHT;
RelativeSizeAxes = Axes.Y;
diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs
index 308359570f..12529da07f 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarClock.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs
@@ -11,7 +11,6 @@ using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
@@ -29,7 +28,6 @@ namespace osu.Game.Overlays.Toolbar
private AnalogClockDisplay analog;
public ToolbarClock()
- : base(HoverSampleSet.Toolbar)
{
RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;
diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs
index 46b8b35da2..929c362bd8 100644
--- a/osu.Game/Overlays/Volume/VolumeMeter.cs
+++ b/osu.Game/Overlays/Volume/VolumeMeter.cs
@@ -329,7 +329,7 @@ namespace osu.Game.Overlays.Volume
if (isPrecise)
{
- scrollAccumulation += delta * adjust_step * 0.1;
+ scrollAccumulation += delta * adjust_step;
while (Precision.AlmostBigger(Math.Abs(scrollAccumulation), precision))
{
diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
index 5e6d9dbe34..aaee15eae8 100644
--- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
@@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Edit
public abstract class DistancedHitObjectComposer : HitObjectComposer, IDistanceSnapProvider, IScrollBindingHandler
where TObject : HitObject
{
+ private const float adjust_step = 0.1f;
+
public Bindable DistanceSpacingMultiplier { get; } = new BindableDouble(1.0)
{
MinValue = 0.1,
@@ -52,7 +54,7 @@ namespace osu.Game.Rulesets.Edit
{
AddInternal(RightSideToolboxContainer = new ExpandingToolboxContainer(130, 250)
{
- Padding = new MarginPadding { Right = 10 },
+ Padding = new MarginPadding(10),
Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
@@ -61,7 +63,7 @@ namespace osu.Game.Rulesets.Edit
Child = distanceSpacingSlider = new ExpandableSlider>
{
Current = { BindTarget = DistanceSpacingMultiplier },
- KeyboardStep = 0.1f,
+ KeyboardStep = adjust_step,
}
}
});
@@ -93,7 +95,7 @@ namespace osu.Game.Rulesets.Edit
{
case GlobalAction.EditorIncreaseDistanceSpacing:
case GlobalAction.EditorDecreaseDistanceSpacing:
- return adjustDistanceSpacing(e.Action, 0.1f);
+ return adjustDistanceSpacing(e.Action, adjust_step);
}
return false;
@@ -109,7 +111,7 @@ namespace osu.Game.Rulesets.Edit
{
case GlobalAction.EditorIncreaseDistanceSpacing:
case GlobalAction.EditorDecreaseDistanceSpacing:
- return adjustDistanceSpacing(e.Action, e.ScrollAmount * (e.IsPrecise ? 0.01f : 0.1f));
+ return adjustDistanceSpacing(e.Action, e.ScrollAmount * adjust_step);
}
return false;
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index f8d796a778..f6fdb228ce 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -114,9 +114,9 @@ namespace osu.Game.Rulesets.Edit
.WithChild(BlueprintContainer = CreateBlueprintContainer())
}
},
- new ExpandingToolboxContainer(80, 200)
+ new ExpandingToolboxContainer(90, 200)
{
- Padding = new MarginPadding { Left = 10 },
+ Padding = new MarginPadding(10),
Children = new Drawable[]
{
new EditorToolboxGroup("toolbox (1-9)")
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index 7cf68a2df7..d3d1196eae 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -340,8 +340,10 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (vertices[endIndex].Position != vertices[endIndex - 1].Position)
continue;
- // Adjacent legacy Catmull segments should be treated as a single segment.
- if (FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION && type == PathType.Catmull)
+ // Legacy Catmull sliders don't support multiple segments, so adjacent Catmull segments should be treated as a single one.
+ // Importantly, this is not applied to the first control point, which may duplicate the slider path's position
+ // resulting in a duplicate (0,0) control point in the resultant list.
+ if (type == PathType.Catmull && endIndex > 1 && FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION)
continue;
// The last control point of each segment is not allowed to start a new implicit segment.
diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs
index 94ddc32bb7..bfa67b8c45 100644
--- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs
@@ -117,9 +117,8 @@ namespace osu.Game.Rulesets.Scoring
///
/// If the provided replay frame does not have any header information, this will be a noop.
///
- /// The ruleset to be used for retrieving statistics.
/// The replay frame to read header statistics from.
- public virtual void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame)
+ public virtual void ResetFromReplayFrame(ReplayFrame frame)
{
if (frame.Header == null)
return;
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index 1dd1d1aeb6..df094ddb7c 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -6,11 +6,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Diagnostics.Contracts;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
+using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -88,17 +90,34 @@ namespace osu.Game.Rulesets.Scoring
private readonly double accuracyPortion;
private readonly double comboPortion;
- private int maxAchievableCombo;
+ ///
+ /// Scoring values for a perfect play.
+ ///
+ public ScoringValues MaximumScoringValues
+ {
+ get
+ {
+ if (!beatmapApplied)
+ throw new InvalidOperationException($"Cannot access maximum scoring values before calling {nameof(ApplyBeatmap)}.");
+
+ return maximumScoringValues;
+ }
+ }
+
+ private ScoringValues maximumScoringValues;
///
- /// The maximum achievable base score.
+ /// Scoring values for the current play assuming all perfect hits.
///
- private double maxBaseScore;
+ ///
+ /// This is only used to determine the accuracy with respect to the current point in time for an ongoing play session.
+ ///
+ private ScoringValues currentMaximumScoringValues;
///
- /// The maximum number of basic (non-tick and non-bonus) hitobjects.
+ /// Scoring values for the current play.
///
- private int maxBasicHitObjects;
+ private ScoringValues currentScoringValues;
///
/// The maximum of a basic (non-tick and non-bonus) hitobject.
@@ -106,9 +125,6 @@ namespace osu.Game.Rulesets.Scoring
///
private HitResult? maxBasicResult;
- private double rollingMaxBaseScore;
- private double baseScore;
- private int basicHitObjects;
private bool beatmapApplied;
private readonly Dictionary scoreResultCounts = new Dictionary();
@@ -163,6 +179,10 @@ namespace osu.Game.Rulesets.Scoring
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1;
+ // Always update the maximum scoring values.
+ applyResult(result.Judgement.MaxResult, ref currentMaximumScoringValues);
+ currentMaximumScoringValues.MaxCombo += result.Judgement.MaxResult.IncreasesCombo() ? 1 : 0;
+
if (!result.Type.IsScorable())
return;
@@ -171,16 +191,8 @@ namespace osu.Game.Rulesets.Scoring
else if (result.Type.BreaksCombo())
Combo.Value = 0;
- double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
-
- if (!result.Type.IsBonus())
- {
- baseScore += scoreIncrease;
- rollingMaxBaseScore += result.Judgement.MaxNumericResult;
- }
-
- if (result.Type.IsBasic())
- basicHitObjects++;
+ applyResult(result.Type, ref currentScoringValues);
+ currentScoringValues.MaxCombo = HighestCombo.Value;
hitEvents.Add(CreateHitEvent(result));
lastHitObject = result.HitObject;
@@ -188,6 +200,20 @@ namespace osu.Game.Rulesets.Scoring
updateScore();
}
+ private static void applyResult(HitResult result, ref ScoringValues scoringValues)
+ {
+ if (!result.IsScorable())
+ return;
+
+ if (result.IsBonus())
+ scoringValues.BonusScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0;
+ else
+ scoringValues.BaseScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0;
+
+ if (result.IsBasic())
+ scoringValues.CountBasicHitObjects++;
+ }
+
///
/// Creates the that describes a .
///
@@ -206,19 +232,15 @@ namespace osu.Game.Rulesets.Scoring
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1;
+ // Always update the maximum scoring values.
+ revertResult(result.Judgement.MaxResult, ref currentMaximumScoringValues);
+ currentMaximumScoringValues.MaxCombo -= result.Judgement.MaxResult.IncreasesCombo() ? 1 : 0;
+
if (!result.Type.IsScorable())
return;
- double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
-
- if (!result.Type.IsBonus())
- {
- baseScore -= scoreIncrease;
- rollingMaxBaseScore -= result.Judgement.MaxNumericResult;
- }
-
- if (result.Type.IsBasic())
- basicHitObjects--;
+ revertResult(result.Type, ref currentScoringValues);
+ currentScoringValues.MaxCombo = HighestCombo.Value;
Debug.Assert(hitEvents.Count > 0);
lastHitObject = hitEvents[^1].LastHitObject;
@@ -227,14 +249,24 @@ namespace osu.Game.Rulesets.Scoring
updateScore();
}
+ private static void revertResult(HitResult result, ref ScoringValues scoringValues)
+ {
+ if (!result.IsScorable())
+ return;
+
+ if (result.IsBonus())
+ scoringValues.BonusScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0;
+ else
+ scoringValues.BaseScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0;
+
+ if (result.IsBasic())
+ scoringValues.CountBasicHitObjects--;
+ }
+
private void updateScore()
{
- double rollingAccuracyRatio = rollingMaxBaseScore > 0 ? baseScore / rollingMaxBaseScore : 1;
- double accuracyRatio = maxBaseScore > 0 ? baseScore / maxBaseScore : 1;
- double comboRatio = maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1;
-
- Accuracy.Value = rollingAccuracyRatio;
- TotalScore.Value = ComputeScore(Mode.Value, accuracyRatio, comboRatio, getBonusScore(scoreResultCounts), maxBasicHitObjects);
+ Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1;
+ TotalScore.Value = ComputeScore(Mode.Value, currentScoringValues, maximumScoringValues);
}
///
@@ -246,22 +278,15 @@ namespace osu.Game.Rulesets.Scoring
/// The to represent the score as.
/// The to compute the total score of.
/// The total score in the given .
+ [Pure]
public double ComputeFinalScore(ScoringMode mode, ScoreInfo scoreInfo)
{
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
- extractFromStatistics(ruleset,
- scoreInfo.Statistics,
- out double extractedBaseScore,
- out double extractedMaxBaseScore,
- out int extractedMaxCombo,
- out int extractedBasicHitObjects);
+ ExtractScoringValues(scoreInfo, out var current, out var maximum);
- double accuracyRatio = extractedMaxBaseScore > 0 ? extractedBaseScore / extractedMaxBaseScore : 1;
- double comboRatio = extractedMaxCombo > 0 ? (double)scoreInfo.MaxCombo / extractedMaxCombo : 1;
-
- return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), extractedBasicHitObjects);
+ return ComputeScore(mode, current, maximum);
}
///
@@ -273,6 +298,7 @@ namespace osu.Game.Rulesets.Scoring
/// The to represent the score as.
/// The to compute the total score of.
/// The total score in the given .
+ [Pure]
public double ComputePartialScore(ScoringMode mode, ScoreInfo scoreInfo)
{
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
@@ -281,17 +307,9 @@ namespace osu.Game.Rulesets.Scoring
if (!beatmapApplied)
throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}.");
- extractFromStatistics(ruleset,
- scoreInfo.Statistics,
- out double extractedBaseScore,
- out _,
- out _,
- out _);
+ ExtractScoringValues(scoreInfo, out var current, out _);
- double accuracyRatio = maxBaseScore > 0 ? extractedBaseScore / maxBaseScore : 1;
- double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1;
-
- return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), maxBasicHitObjects);
+ return ComputeScore(mode, current, MaximumScoringValues);
}
///
@@ -305,6 +323,7 @@ namespace osu.Game.Rulesets.Scoring
/// The to compute the total score of.
/// The maximum achievable combo for the provided beatmap.
/// The total score in the given .
+ [Pure]
public double ComputeFinalLegacyScore(ScoringMode mode, ScoreInfo scoreInfo, int maxAchievableCombo)
{
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
@@ -313,26 +332,30 @@ namespace osu.Game.Rulesets.Scoring
double accuracyRatio = scoreInfo.Accuracy;
double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1;
+ ExtractScoringValues(scoreInfo, out var current, out var maximum);
+
// For legacy osu!mania scores, a full-GREAT score has 100% accuracy. If combined with a full-combo, the score becomes indistinguishable from a full-PERFECT score.
// To get around this, the accuracy ratio is always recalculated based on the hit statistics rather than trusting the score.
// Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together.
- if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3)
- {
- extractFromStatistics(
- ruleset,
- scoreInfo.Statistics,
- out double computedBaseScore,
- out double computedMaxBaseScore,
- out _,
- out _);
+ if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3 && maximum.BaseScore > 0)
+ accuracyRatio = current.BaseScore / maximum.BaseScore;
- if (computedMaxBaseScore > 0)
- accuracyRatio = computedBaseScore / computedMaxBaseScore;
- }
+ return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects);
+ }
- int computedBasicHitObjects = scoreInfo.Statistics.Where(kvp => kvp.Key.IsBasic()).Select(kvp => kvp.Value).Sum();
-
- return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), computedBasicHitObjects);
+ ///
+ /// Computes the total score from scoring values.
+ ///
+ /// The to represent the score as.
+ /// The current scoring values.
+ /// The maximum scoring values.
+ /// The total score computed from the given scoring values.
+ [Pure]
+ public double ComputeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum)
+ {
+ double accuracyRatio = maximum.BaseScore > 0 ? current.BaseScore / maximum.BaseScore : 1;
+ double comboRatio = maximum.MaxCombo > 0 ? (double)current.MaxCombo / maximum.MaxCombo : 1;
+ return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects);
}
///
@@ -344,6 +367,7 @@ namespace osu.Game.Rulesets.Scoring
/// The total bonus score.
/// The total number of basic (non-tick and non-bonus) hitobjects in the beatmap.
/// The total score computed from the given scoring component ratios.
+ [Pure]
public double ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, double bonusScore, int totalBasicHitObjects)
{
switch (mode)
@@ -362,15 +386,6 @@ namespace osu.Game.Rulesets.Scoring
}
}
- ///
- /// Calculates the total bonus score from score statistics.
- ///
- /// The score statistics.
- /// The total bonus score.
- private double getBonusScore(IReadOnlyDictionary statistics)
- => statistics.GetValueOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE
- + statistics.GetValueOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE;
-
private ScoreRank rankFrom(double acc)
{
if (acc == 1)
@@ -402,15 +417,10 @@ namespace osu.Game.Rulesets.Scoring
lastHitObject = null;
if (storeResults)
- {
- maxAchievableCombo = HighestCombo.Value;
- maxBaseScore = baseScore;
- maxBasicHitObjects = basicHitObjects;
- }
+ maximumScoringValues = currentScoringValues;
- baseScore = 0;
- rollingMaxBaseScore = 0;
- basicHitObjects = 0;
+ currentScoringValues = default;
+ currentMaximumScoringValues = default;
TotalScore.Value = 0;
Accuracy.Value = 1;
@@ -437,14 +447,20 @@ namespace osu.Game.Rulesets.Scoring
score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score));
}
- public override void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame)
+ public override void ResetFromReplayFrame(ReplayFrame frame)
{
- base.ResetFromReplayFrame(ruleset, frame);
+ base.ResetFromReplayFrame(frame);
if (frame.Header == null)
return;
- extractFromStatistics(ruleset, frame.Header.Statistics, out baseScore, out rollingMaxBaseScore, out _, out _);
+ extractScoringValues(frame.Header.Statistics, out var current, out var maximum);
+ currentScoringValues.BaseScore = current.BaseScore;
+ currentScoringValues.MaxCombo = frame.Header.MaxCombo;
+ currentMaximumScoringValues.BaseScore = maximum.BaseScore;
+ currentMaximumScoringValues.MaxCombo = maximum.MaxCombo;
+
+ Combo.Value = frame.Header.Combo;
HighestCombo.Value = frame.Header.MaxCombo;
scoreResultCounts.Clear();
@@ -455,52 +471,126 @@ namespace osu.Game.Rulesets.Scoring
OnResetFromReplayFrame?.Invoke();
}
- private void extractFromStatistics(Ruleset ruleset, IReadOnlyDictionary statistics, out double baseScore, out double maxBaseScore, out int maxCombo,
- out int basicHitObjects)
+ #region ScoringValue extraction
+
+ ///
+ /// Applies a best-effort extraction of hit statistics into .
+ ///
+ ///
+ /// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
+ ///
+ /// - The maximum will always be 0.
+ /// - The current and maximum will always be the same value.
+ ///
+ /// Consumers are expected to more accurately fill in the above values through external means.
+ ///
+ /// Ensure to fill in the maximum for use in
+ /// .
+ ///
+ ///
+ /// The score to extract scoring values from.
+ /// The "current" scoring values, representing the hit statistics as they appear.
+ /// The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.
+ [Pure]
+ internal void ExtractScoringValues(ScoreInfo scoreInfo, out ScoringValues current, out ScoringValues maximum)
{
- baseScore = 0;
- maxBaseScore = 0;
- maxCombo = 0;
- basicHitObjects = 0;
+ extractScoringValues(scoreInfo.Statistics, out current, out maximum);
+ current.MaxCombo = scoreInfo.MaxCombo;
+ }
+
+ ///
+ /// Applies a best-effort extraction of hit statistics into .
+ ///
+ ///
+ /// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
+ ///
+ /// - The maximum will always be 0.
+ /// - The current and maximum will always be the same value.
+ ///
+ /// Consumers are expected to more accurately fill in the above values through external means.
+ ///
+ /// Ensure to fill in the maximum for use in
+ /// .
+ ///
+ ///
+ /// The replay frame header to extract scoring values from.
+ /// The "current" scoring values, representing the hit statistics as they appear.
+ /// The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.
+ [Pure]
+ internal void ExtractScoringValues(FrameHeader header, out ScoringValues current, out ScoringValues maximum)
+ {
+ extractScoringValues(header.Statistics, out current, out maximum);
+ current.MaxCombo = header.MaxCombo;
+ }
+
+ ///
+ /// Applies a best-effort extraction of hit statistics into .
+ ///
+ ///
+ /// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
+ ///
+ /// - The current will always be 0.
+ /// - The maximum will always be 0.
+ /// - The current and maximum will always be the same value.
+ ///
+ /// Consumers are expected to more accurately fill in the above values (especially the current ) via external means (e.g. ).
+ ///
+ /// The hit statistics to extract scoring values from.
+ /// The "current" scoring values, representing the hit statistics as they appear.
+ /// The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.
+ [Pure]
+ private void extractScoringValues(IReadOnlyDictionary statistics, out ScoringValues current, out ScoringValues maximum)
+ {
+ current = default;
+ maximum = default;
foreach ((HitResult result, int count) in statistics)
{
- // Bonus scores are counted separately directly from the statistics dictionary later on.
- if (!result.IsScorable() || result.IsBonus())
+ if (!result.IsScorable())
continue;
- // The maximum result of this judgement if it wasn't a miss.
- // E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT).
- HitResult maxResult;
-
- switch (result)
+ if (result.IsBonus())
+ current.BonusScore += count * Judgement.ToNumericResult(result);
+ else
{
- case HitResult.LargeTickHit:
- case HitResult.LargeTickMiss:
- maxResult = HitResult.LargeTickHit;
- break;
+ // The maximum result of this judgement if it wasn't a miss.
+ // E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT).
+ HitResult maxResult;
- case HitResult.SmallTickHit:
- case HitResult.SmallTickMiss:
- maxResult = HitResult.SmallTickHit;
- break;
+ switch (result)
+ {
+ case HitResult.LargeTickHit:
+ case HitResult.LargeTickMiss:
+ maxResult = HitResult.LargeTickHit;
+ break;
- default:
- maxResult = maxBasicResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result;
- break;
+ case HitResult.SmallTickHit:
+ case HitResult.SmallTickMiss:
+ maxResult = HitResult.SmallTickHit;
+ break;
+
+ default:
+ maxResult = maxBasicResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result;
+ break;
+ }
+
+ current.BaseScore += count * Judgement.ToNumericResult(result);
+ maximum.BaseScore += count * Judgement.ToNumericResult(maxResult);
}
- baseScore += count * Judgement.ToNumericResult(result);
- maxBaseScore += count * Judgement.ToNumericResult(maxResult);
-
if (result.AffectsCombo())
- maxCombo += count;
+ maximum.MaxCombo += count;
if (result.IsBasic())
- basicHitObjects += count;
+ {
+ current.CountBasicHitObjects += count;
+ maximum.CountBasicHitObjects += count;
+ }
}
}
+ #endregion
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs
index 7d1b23f48b..b5390eb6e2 100644
--- a/osu.Game/Rulesets/UI/RulesetInputManager.cs
+++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs
@@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.UI
{
public readonly KeyBindingContainer KeyBindingContainer;
- private readonly Ruleset ruleset;
-
[Resolved(CanBeNull = true)]
private ScoreProcessor scoreProcessor { get; set; }
@@ -57,8 +55,6 @@ namespace osu.Game.Rulesets.UI
protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
{
- this.ruleset = ruleset.CreateInstance();
-
InternalChild = KeyBindingContainer =
CreateKeyBindingContainer(ruleset, variant, unique)
.WithChild(content = new Container { RelativeSizeAxes = Axes.Both });
@@ -85,7 +81,7 @@ namespace osu.Game.Rulesets.UI
break;
case ReplayStatisticsFrameEvent statisticsStateChangeEvent:
- scoreProcessor?.ResetFromReplayFrame(ruleset, statisticsStateChangeEvent.Frame);
+ scoreProcessor?.ResetFromReplayFrame(statisticsStateChangeEvent.Frame);
break;
default:
diff --git a/osu.Game/Scoring/ScoringValues.cs b/osu.Game/Scoring/ScoringValues.cs
new file mode 100644
index 0000000000..d31cd7c68b
--- /dev/null
+++ b/osu.Game/Scoring/ScoringValues.cs
@@ -0,0 +1,41 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using MessagePack;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Scoring
+{
+ ///
+ /// Stores the required scoring data that fulfils the minimum requirements for a to calculate score.
+ ///
+ [MessagePackObject]
+ public struct ScoringValues
+ {
+ ///
+ /// The sum of all "basic" scoring values. See: and .
+ ///
+ [Key(0)]
+ public double BaseScore;
+
+ ///
+ /// The sum of all "bonus" scoring values. See: and .
+ ///
+ [Key(1)]
+ public double BonusScore;
+
+ ///
+ /// The highest achieved combo.
+ ///
+ [Key(2)]
+ public int MaxCombo;
+
+ ///
+ /// The count of "basic" s. See: .
+ ///
+ [Key(3)]
+ public int CountBasicHitObjects;
+ }
+}
diff --git a/osu.Game/Screens/Edit/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/BindableBeatDivisor.cs
index af958e3448..8f430dce77 100644
--- a/osu.Game/Screens/Edit/BindableBeatDivisor.cs
+++ b/osu.Game/Screens/Edit/BindableBeatDivisor.cs
@@ -5,6 +5,7 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit
@@ -100,6 +101,32 @@ namespace osu.Game.Screens.Edit
}
}
+ ///
+ /// Get a relative display size for the specified divisor.
+ ///
+ /// The beat divisor.
+ /// A relative size which can be used to display ticks.
+ public static Vector2 GetSize(int beatDivisor)
+ {
+ switch (beatDivisor)
+ {
+ case 1:
+ case 2:
+ return new Vector2(0.6f, 0.9f);
+
+ case 3:
+ case 4:
+ return new Vector2(0.5f, 0.8f);
+
+ case 6:
+ case 8:
+ return new Vector2(0.4f, 0.7f);
+
+ default:
+ return new Vector2(0.3f, 0.6f);
+ }
+ }
+
///
/// Retrieves the applicable divisor for a specific beat index.
///
diff --git a/osu.Game/Screens/Edit/BottomBar.cs b/osu.Game/Screens/Edit/BottomBar.cs
new file mode 100644
index 0000000000..62caaced89
--- /dev/null
+++ b/osu.Game/Screens/Edit/BottomBar.cs
@@ -0,0 +1,81 @@
+// 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.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Overlays;
+using osu.Game.Screens.Edit.Components;
+using osu.Game.Screens.Edit.Components.Timelines.Summary;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Edit
+{
+ internal class BottomBar : CompositeDrawable
+ {
+ public TestGameplayButton TestGameplayButton { get; private set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider, Editor editor)
+ {
+ Anchor = Anchor.BottomLeft;
+ Origin = Anchor.BottomLeft;
+
+ RelativeSizeAxes = Axes.X;
+
+ Height = 60;
+
+ Masking = true;
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Colour = Color4.Black.Opacity(0.2f),
+ Type = EdgeEffectType.Shadow,
+ Radius = 10f,
+ };
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background4,
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ColumnDimensions = new[]
+ {
+ new Dimension(GridSizeMode.Absolute, 170),
+ new Dimension(),
+ new Dimension(GridSizeMode.Absolute, 220),
+ new Dimension(GridSizeMode.Absolute, 120),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new TimeInfoContainer { RelativeSizeAxes = Axes.Both },
+ new SummaryTimeline { RelativeSizeAxes = Axes.Both },
+ new PlaybackControl { RelativeSizeAxes = Axes.Both },
+ TestGameplayButton = new TestGameplayButton
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Left = 10 },
+ Size = new Vector2(1),
+ Action = editor.TestGameplay,
+ }
+ },
+ }
+ },
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs
index 08091fc3f7..3c63da3a4a 100644
--- a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs
+++ b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs
@@ -8,20 +8,19 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
-using osu.Game.Graphics;
+using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components
{
public class BottomBarContainer : Container
{
- private const float corner_radius = 5;
private const float contents_padding = 15;
protected readonly IBindable Beatmap = new Bindable();
protected readonly IBindable