diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 65ac05261a..cbd0231fdb 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -21,7 +21,7 @@
]
},
"ppy.localisationanalyser.tools": {
- "version": "2022.417.0",
+ "version": "2022.607.0",
"commands": [
"localisation"
]
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 c28085557e..aad8cf10d0 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,10 +52,10 @@
-
+
-
+
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/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/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index a904658a4c..fa095edafa 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -79,7 +79,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
Result = { BindTarget = SpinsPerMinute },
},
- ticks = new Container(),
+ ticks = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
new AspectContainer
{
Anchor = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
index 726fbd3ea6..39239c8233 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.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 osu.Framework.Graphics;
+
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSpinnerTick : DrawableOsuHitObject
@@ -10,13 +12,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected DrawableSpinner DrawableSpinner => (DrawableSpinner)ParentHitObject;
public DrawableSpinnerTick()
- : base(null)
+ : this(null)
{
}
public DrawableSpinnerTick(SpinnerTick spinnerTick)
: base(spinnerTick)
{
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
}
protected override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;
diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
index ddee4d3ebd..1a130e96b3 100644
--- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
@@ -69,8 +69,8 @@ namespace osu.Game.Rulesets.Osu.Objects
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
AddNested(i < SpinsRequired
- ? new SpinnerTick { StartTime = startTime, Position = Position }
- : new SpinnerBonusTick { StartTime = startTime, Position = Position });
+ ? new SpinnerTick { StartTime = startTime }
+ : new SpinnerBonusTick { StartTime = startTime });
}
}
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.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index 504b10e9bc..2dd332fc13 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
+using osu.Framework.Graphics.Primitives;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
@@ -321,12 +322,14 @@ namespace osu.Game.Rulesets.Taiko.UI
private class ProxyContainer : LifetimeManagementContainer
{
- public new MarginPadding Padding
- {
- set => base.Padding = value;
- }
-
public void Add(Drawable proxy) => AddInternal(proxy);
+
+ public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds)
+ {
+ // DrawableHitObject disables masking.
+ // Hitobject content is proxied and unproxied based on hit status and the IsMaskedAway value could get stuck because of this.
+ return false;
+ }
}
}
}
diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs
index eaacc623c9..2bb6459f20 100644
--- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs
+++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Chat
[SetUp]
public void Setup() => Schedule(() =>
{
- var container = new ChannelManagerContainer();
+ var container = new ChannelManagerContainer(API);
Child = container;
channelManager = container.ChannelManager;
});
@@ -145,11 +145,11 @@ namespace osu.Game.Tests.Chat
private class ChannelManagerContainer : CompositeDrawable
{
[Cached]
- public ChannelManager ChannelManager { get; } = new ChannelManager();
+ public ChannelManager ChannelManager { get; }
- public ChannelManagerContainer()
+ public ChannelManagerContainer(IAPIProvider apiProvider)
{
- InternalChild = ChannelManager;
+ InternalChild = ChannelManager = new ChannelManager(apiProvider);
}
}
}
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/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
index d3cacaa88c..d68398236a 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
@@ -59,11 +59,13 @@ namespace osu.Game.Tests.Skins
AddAssert("Check float parse lookup", () => requester.GetConfig("FloatTest")?.Value == 1.1f);
}
- [Test]
- public void TestBoolLookup()
+ [TestCase("0", false)]
+ [TestCase("1", true)]
+ [TestCase("2", true)] // https://github.com/ppy/osu/issues/18579
+ public void TestBoolLookup(string originalValue, bool expectedParsedValue)
{
- AddStep("Add config values", () => userSource.Configuration.ConfigDictionary["BoolTest"] = "1");
- AddAssert("Check bool parse lookup", () => requester.GetConfig("BoolTest")?.Value == true);
+ AddStep("Add config values", () => userSource.Configuration.ConfigDictionary["BoolTest"] = originalValue);
+ AddAssert("Check bool parse lookup", () => requester.GetConfig("BoolTest")?.Value == expectedParsedValue);
}
[Test]
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 46b45979ea..a1218aa3e7 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs
@@ -11,7 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing;
@@ -77,28 +77,6 @@ namespace osu.Game.Tests.Visual.Editing
timingInfo.Text = $"offset: {selectedGroup.Value.Time:N2} bpm: {selectedGroup.Value.ControlPoints.OfType().First().BPM:N2}";
}
- [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);
- }
-
[Test]
public void TestBasic()
{
@@ -109,7 +87,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("click tap button", () =>
{
- control.ChildrenOfType()
+ control.ChildrenOfType()
.Last()
.TriggerClick();
});
@@ -123,6 +101,28 @@ namespace osu.Game.Tests.Visual.Editing
});
}
+ [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)
{
Beatmap.Disabled = false;
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/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs
index 4e6342868a..a738debecc 100644
--- a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs
@@ -18,7 +18,6 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
-using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
@@ -27,7 +26,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public abstract class MultiplayerGameplayLeaderboardTestScene : OsuTestScene
{
- private const int total_users = 16;
+ protected const int TOTAL_USERS = 16;
protected readonly BindableList MultiplayerUsers = new BindableList();
@@ -35,9 +34,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected virtual MultiplayerRoomUser CreateUser(int userId) => new MultiplayerRoomUser(userId);
- protected abstract MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor);
+ protected abstract MultiplayerGameplayLeaderboard CreateLeaderboard();
private readonly BindableList multiplayerUserIds = new BindableList();
+ private readonly BindableDictionary watchedUserStates = new BindableDictionary();
private OsuConfigManager config;
@@ -81,6 +81,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
multiplayerClient.SetupGet(c => c.CurrentMatchPlayingUserIds)
.Returns(() => multiplayerUserIds);
+
+ spectatorClient.SetupGet(c => c.WatchedUserStates)
+ .Returns(() => watchedUserStates);
}
[SetUpSteps]
@@ -100,8 +103,26 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("populate users", () =>
{
MultiplayerUsers.Clear();
- for (int i = 0; i < total_users; i++)
- MultiplayerUsers.Add(CreateUser(i));
+
+ for (int i = 0; i < TOTAL_USERS; i++)
+ {
+ var user = CreateUser(i);
+
+ MultiplayerUsers.Add(user);
+
+ watchedUserStates[i] = new SpectatorState
+ {
+ BeatmapID = 0,
+ RulesetID = 0,
+ Mods = user.Mods,
+ MaximumScoringValues = new ScoringValues
+ {
+ BaseScore = 10000,
+ MaxCombo = 1000,
+ CountBasicHitObjects = 1000
+ }
+ };
+ }
});
AddStep("create leaderboard", () =>
@@ -109,13 +130,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
Leaderboard?.Expire();
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
- var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
- OsuScoreProcessor scoreProcessor = new OsuScoreProcessor();
- scoreProcessor.ApplyBeatmap(playableBeatmap);
- Child = scoreProcessor;
-
- LoadComponentAsync(Leaderboard = CreateLeaderboard(scoreProcessor), Add);
+ LoadComponentAsync(Leaderboard = CreateLeaderboard(), Add);
});
AddUntilStep("wait for load", () => Leaderboard.IsLoaded);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
index f57a54d84c..60215dc8b3 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
@@ -4,11 +4,11 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
-using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play.HUD;
@@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("reset", () =>
{
- Clear();
+ leaderboard?.RemoveAndDisposeImmediately();
clocks = new Dictionary
{
@@ -32,21 +32,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ PLAYER_2_ID, new ManualClock() }
};
- foreach ((int userId, var _) in clocks)
+ foreach ((int userId, _) in clocks)
{
SpectatorClient.SendStartPlay(userId, 0);
- OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = userId });
+ OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = userId }, true);
}
});
AddStep("create leaderboard", () =>
{
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
- var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
- var scoreProcessor = new OsuScoreProcessor();
- scoreProcessor.ApplyBeatmap(playable);
- LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(Ruleset.Value, scoreProcessor, clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray())
+ LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray())
{
Expanded = { Value = true }
}, Add);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
index 6e4aa48b0e..cbbd535cee 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
@@ -1,22 +1,58 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Osu.Scoring;
+using osu.Game.Online.API;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerGameplayLeaderboardTestScene
{
- protected override MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor)
+ protected override MultiplayerRoomUser CreateUser(int userId)
{
- return new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, MultiplayerUsers.ToArray())
+ var user = base.CreateUser(userId);
+
+ if (userId == TOTAL_USERS - 1)
+ user.Mods = new[] { new APIMod(new OsuModNoFail()) };
+
+ return user;
+ }
+
+ protected override MultiplayerGameplayLeaderboard CreateLeaderboard()
+ {
+ return new TestLeaderboard(MultiplayerUsers.ToArray())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
}
+
+ [Test]
+ public void TestPerUserMods()
+ {
+ AddStep("first user has no mods", () => Assert.That(((TestLeaderboard)Leaderboard).UserMods[0], Is.Empty));
+ AddStep("last user has NF mod", () =>
+ {
+ Assert.That(((TestLeaderboard)Leaderboard).UserMods[TOTAL_USERS - 1], Has.One.Items);
+ Assert.That(((TestLeaderboard)Leaderboard).UserMods[TOTAL_USERS - 1].Single(), Is.TypeOf());
+ });
+ }
+
+ private class TestLeaderboard : MultiplayerGameplayLeaderboard
+ {
+ public Dictionary> UserMods => UserScores.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ScoreProcessor.Mods);
+
+ public TestLeaderboard(MultiplayerRoomUser[] users)
+ : base(users)
+ {
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs
index 5caab9487e..c25884039f 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs
@@ -5,7 +5,6 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
-using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play.HUD;
@@ -25,8 +24,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
return user;
}
- protected override MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor) =>
- new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, MultiplayerUsers.ToArray())
+ protected override MultiplayerGameplayLeaderboard CreateLeaderboard() =>
+ new MultiplayerGameplayLeaderboard(MultiplayerUsers.ToArray())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index c1f5f110d1..51bb27f93e 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -609,8 +609,6 @@ namespace osu.Game.Tests.Visual.Navigation
public ModSelectOverlay ModSelectOverlay => ModSelect;
public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions;
-
- protected override bool DisplayStableImportPrompt => false;
}
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
index a28de3be1e..4d227af2cb 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Online
{
linkColour = colours.Blue;
- var chatManager = new ChannelManager();
+ var chatManager = new ChannelManager(API);
BindableList availableChannels = (BindableList)chatManager.AvailableChannels;
availableChannels.Add(new Channel { Name = "#english" });
availableChannels.Add(new Channel { Name = "#japanese" });
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
index 4edbb9f215..e3792c0780 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
@@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
- (typeof(ChannelManager), channelManager = new ChannelManager()),
+ (typeof(ChannelManager), channelManager = new ChannelManager(API)),
},
Children = new Drawable[]
{
@@ -469,6 +469,8 @@ namespace osu.Game.Tests.Visual.Online
chatOverlay.Show();
});
+ AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
+
waitForChannel1Visible();
AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext));
waitForChannel2Visible();
@@ -570,15 +572,15 @@ namespace osu.Game.Tests.Visual.Online
public SlowLoadingDrawableChannel GetSlowLoadingChannel(Channel channel) => DrawableChannels.OfType().Single(c => c.Channel == channel);
- protected override ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel)
+ protected override DrawableChannel CreateDrawableChannel(Channel newChannel)
{
return SlowLoading
? new SlowLoadingDrawableChannel(newChannel)
- : new ChatOverlayDrawableChannel(newChannel);
+ : new DrawableChannel(newChannel);
}
}
- private class SlowLoadingDrawableChannel : ChatOverlayDrawableChannel
+ private class SlowLoadingDrawableChannel : DrawableChannel
{
public readonly ManualResetEventSlim LoadEvent = new ManualResetEventSlim();
diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs
index 79f62a16e3..c7ca3b4457 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -43,7 +44,7 @@ namespace osu.Game.Tests.Visual.Online
Schedule(() =>
{
- Child = testContainer = new TestContainer(new[] { publicChannel, privateMessageChannel })
+ Child = testContainer = new TestContainer(API, new[] { publicChannel, privateMessageChannel })
{
RelativeSizeAxes = Axes.Both,
};
@@ -178,6 +179,36 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("1 notification fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 1);
}
+ ///
+ /// Ensures that handles channels which have not been or could not be resolved (i.e. = 0).
+ ///
+ [Test]
+ public void TestSendInUnresolvedChannel()
+ {
+ int i = 1;
+ Channel unresolved = null;
+
+ AddRepeatStep("join unresolved channels", () => testContainer.ChannelManager.JoinChannel(unresolved = new Channel(new APIUser
+ {
+ Id = 100 + i,
+ Username = $"Foreign #{i++}",
+ })), 5);
+
+ AddStep("send message in unresolved channel", () =>
+ {
+ Debug.Assert(unresolved.Id == 0);
+
+ unresolved.AddLocalEcho(new LocalEchoMessage
+ {
+ Sender = API.LocalUser.Value,
+ ChannelId = unresolved.Id,
+ Content = "Some message",
+ });
+ });
+
+ AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0);
+ }
+
private void receiveMessage(APIUser sender, Channel channel, string content) => channel.AddNewMessages(createMessage(sender, channel, content));
private Message createMessage(APIUser sender, Channel channel, string content) => new Message(messageIdCounter++)
@@ -198,7 +229,7 @@ namespace osu.Game.Tests.Visual.Online
private class TestContainer : Container
{
[Cached]
- public ChannelManager ChannelManager { get; } = new ChannelManager();
+ public ChannelManager ChannelManager { get; }
[Cached(typeof(INotificationOverlay))]
public NotificationOverlay NotificationOverlay { get; } = new NotificationOverlay
@@ -214,9 +245,10 @@ namespace osu.Game.Tests.Visual.Online
private readonly Channel[] channels;
- public TestContainer(Channel[] channels)
+ public TestContainer(IAPIProvider api, Channel[] channels)
{
this.channels = channels;
+ ChannelManager = new ChannelManager(api);
}
[BackgroundDependencyLoader]
diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
index 860ef5d565..8d5eebd31f 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs
@@ -11,6 +11,7 @@ using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
+using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Chat;
using osuTK.Input;
@@ -44,17 +45,22 @@ namespace osu.Game.Tests.Visual.Online
Id = 5,
};
- [Cached]
- private ChannelManager channelManager = new ChannelManager();
+ private ChannelManager channelManager;
private TestStandAloneChatDisplay chatDisplay;
private int messageIdSequence;
private Channel testChannel;
- public TestSceneStandAloneChatDisplay()
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
- Add(channelManager);
+ Add(channelManager = new ChannelManager(parent.Get()));
+
+ var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
+
+ dependencies.Cache(channelManager);
+
+ return dependencies;
}
[SetUp]
@@ -128,11 +134,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/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
index 8b646df362..2a31728f87 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
@@ -96,6 +96,7 @@ namespace osu.Game.Tests.Visual.Ranking
beatmap.Metadata.Author = author;
beatmap.Metadata.Title = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap title";
beatmap.Metadata.Artist = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap artist";
+ beatmap.DifficultyName = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong difficulty name";
return beatmap;
}
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/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
index 940d001c5b..a78a8aa028 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
@@ -5,12 +5,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
-using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
@@ -23,38 +22,28 @@ namespace osu.Game.Tests.Visual.SongSelect
{
public class TestSceneBeatmapRecommendations : OsuGameTestScene
{
+ [Resolved]
+ private IRulesetStore rulesetStore { get; set; }
+
[SetUpSteps]
public override void SetUpSteps()
{
- AddStep("register request handling", () =>
- {
- ((DummyAPIAccess)API).HandleRequest = req =>
- {
- switch (req)
- {
- case GetUserRequest userRequest:
- userRequest.TriggerSuccess(getUser(userRequest.Ruleset.OnlineID));
- return true;
- }
-
- return false;
- };
- });
-
base.SetUpSteps();
- APIUser getUser(int? rulesetID)
+ AddStep("populate ruleset statistics", () =>
{
- return new APIUser
+ Dictionary rulesetStatistics = new Dictionary();
+
+ rulesetStore.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo =>
{
- Username = @"Dummy",
- Id = 1001,
- Statistics = new UserStatistics
+ rulesetStatistics[rulesetInfo.ShortName] = new UserStatistics
{
- PP = getNecessaryPP(rulesetID)
- }
- };
- }
+ PP = getNecessaryPP(rulesetInfo.OnlineID)
+ };
+ });
+
+ API.LocalUser.Value.RulesetsStatistics = rulesetStatistics;
+ });
decimal getNecessaryPP(int? rulesetID)
{
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index aad7f6b301..77f5bd83d6 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -18,6 +18,7 @@ using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Chat;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
@@ -80,6 +81,37 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("delete all beatmaps", () => manager?.Delete());
}
+ [Test]
+ public void TestPlaceholderBeatmapPresence()
+ {
+ createSongSelect();
+
+ AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
+
+ addRulesetImportStep(0);
+ AddUntilStep("wait for placeholder hidden", () => getPlaceholder()?.State.Value == Visibility.Hidden);
+
+ AddStep("delete all beatmaps", () => manager?.Delete());
+ AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
+ }
+
+ [Test]
+ public void TestPlaceholderConvertSetting()
+ {
+ changeRuleset(2);
+ addRulesetImportStep(0);
+ AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
+
+ createSongSelect();
+
+ AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
+
+ AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType().First().TriggerClick());
+
+ AddUntilStep("convert setting changed", () => config.Get(OsuSetting.ShowConvertedBeatmaps));
+ AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden);
+ }
+
[Test]
public void TestSingleFilterOnEnter()
{
@@ -941,6 +973,8 @@ namespace osu.Game.Tests.Visual.SongSelect
private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.IndexOf(info);
+ private NoResultsPlaceholder getPlaceholder() => songSelect.ChildrenOfType().FirstOrDefault();
+
private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmapInfo);
private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, FilterableDifficultyIcon icon)
diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs
index a5ead6c2f0..c30250c86a 100644
--- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs
+++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs
@@ -5,6 +5,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Game.Online.API;
using osu.Game.Online.Chat;
using osu.Game.Overlays.Chat;
using osu.Game.Tournament.IPC;
@@ -29,7 +30,7 @@ namespace osu.Game.Tournament.Components
}
[BackgroundDependencyLoader(true)]
- private void load(MatchIPCInfo ipc)
+ private void load(MatchIPCInfo ipc, IAPIProvider api)
{
if (ipc != null)
{
@@ -45,7 +46,7 @@ namespace osu.Game.Tournament.Components
if (manager == null)
{
- AddInternal(manager = new ChannelManager { HighPollRate = { Value = true } });
+ AddInternal(manager = new ChannelManager(api) { HighPollRate = { Value = true } });
Channel.BindTo(manager.CurrentChannel);
}
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/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs
index 93c2fccbc7..4629b20569 100644
--- a/osu.Game/Beatmaps/DifficultyRecommender.cs
+++ b/osu.Game/Beatmaps/DifficultyRecommender.cs
@@ -7,11 +7,8 @@ using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
-using osu.Game.Extensions;
using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
namespace osu.Game.Beatmaps
@@ -25,26 +22,15 @@ namespace osu.Game.Beatmaps
[Resolved]
private IAPIProvider api { get; set; }
- [Resolved]
- private IRulesetStore rulesets { get; set; }
-
[Resolved]
private Bindable ruleset { get; set; }
- ///
- /// The user for which the last requests were run.
- ///
- private int? requestedUserId;
-
- private readonly Dictionary recommendedDifficultyMapping = new Dictionary();
-
- private readonly IBindable apiState = new Bindable();
+ private readonly Dictionary recommendedDifficultyMapping = new Dictionary();
[BackgroundDependencyLoader]
private void load()
{
- apiState.BindTo(api.State);
- apiState.BindValueChanged(onlineStateChanged, true);
+ api.LocalUser.BindValueChanged(_ => populateValues(), true);
}
///
@@ -58,12 +44,12 @@ namespace osu.Game.Beatmaps
[CanBeNull]
public BeatmapInfo GetRecommendedBeatmap(IEnumerable beatmaps)
{
- foreach (var r in orderedRulesets)
+ foreach (string r in orderedRulesets)
{
if (!recommendedDifficultyMapping.TryGetValue(r, out double recommendation))
continue;
- BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b =>
+ BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.ShortName.Equals(r)).OrderBy(b =>
{
double difference = b.StarRating - recommendation;
return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder
@@ -76,55 +62,35 @@ namespace osu.Game.Beatmaps
return null;
}
- private void fetchRecommendedValues()
+ private void populateValues()
{
- if (recommendedDifficultyMapping.Count > 0 && api.LocalUser.Value.Id == requestedUserId)
+ if (api.LocalUser.Value.RulesetsStatistics == null)
return;
- requestedUserId = api.LocalUser.Value.Id;
-
- // only query API for built-in rulesets
- rulesets.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo =>
+ foreach (var kvp in api.LocalUser.Value.RulesetsStatistics)
{
- var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo);
-
- req.Success += result =>
- {
- // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
- recommendedDifficultyMapping[rulesetInfo] = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195;
- };
-
- api.Queue(req);
- });
+ // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
+ recommendedDifficultyMapping[kvp.Key] = Math.Pow((double)(kvp.Value.PP ?? 0), 0.4) * 0.195;
+ }
}
///
/// Rulesets ordered descending by their respective recommended difficulties.
/// The currently selected ruleset will always be first.
///
- private IEnumerable orderedRulesets
+ private IEnumerable orderedRulesets
{
get
{
if (LoadState < LoadState.Ready || ruleset.Value == null)
- return Enumerable.Empty();
+ return Enumerable.Empty();
return recommendedDifficultyMapping
.OrderByDescending(pair => pair.Value)
.Select(pair => pair.Key)
- .Where(r => !r.Equals(ruleset.Value))
- .Prepend(ruleset.Value);
+ .Where(r => !r.Equals(ruleset.Value.ShortName))
+ .Prepend(ruleset.Value.ShortName);
}
}
-
- private void onlineStateChanged(ValueChangedEvent state) => Schedule(() =>
- {
- switch (state.NewValue)
- {
- case APIState.Online:
- fetchRecommendedValues();
- break;
- }
- });
}
}
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/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/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs
index 11bfd80ec1..5c6e315225 100644
--- a/osu.Game/Graphics/Containers/ScalingContainer.cs
+++ b/osu.Game/Graphics/Containers/ScalingContainer.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Graphics.Containers
///
public class ScalingContainer : Container
{
- private const float duration = 500;
+ internal const float TRANSITION_DURATION = 500;
private Bindable sizeX;
private Bindable sizeY;
@@ -99,7 +99,7 @@ namespace osu.Game.Graphics.Containers
if (applyUIScale)
{
uiScale = osuConfig.GetBindable(OsuSetting.UIScale);
- uiScale.BindValueChanged(args => this.TransformTo(nameof(CurrentScale), args.NewValue, duration, Easing.OutQuart), true);
+ uiScale.BindValueChanged(args => this.TransformTo(nameof(CurrentScale), args.NewValue, TRANSITION_DURATION, Easing.OutQuart), true);
}
}
@@ -163,10 +163,10 @@ namespace osu.Game.Graphics.Containers
backgroundStack.Push(new ScalingBackgroundScreen());
}
- backgroundStack.FadeIn(duration);
+ backgroundStack.FadeIn(TRANSITION_DURATION);
}
else
- backgroundStack?.FadeOut(duration);
+ backgroundStack?.FadeOut(TRANSITION_DURATION);
}
RectangleF targetRect = new RectangleF(Vector2.Zero, Vector2.One);
@@ -195,13 +195,13 @@ namespace osu.Game.Graphics.Containers
if (requiresMasking)
sizableContainer.Masking = true;
- sizableContainer.MoveTo(targetRect.Location, duration, Easing.OutQuart);
- sizableContainer.ResizeTo(targetRect.Size, duration, Easing.OutQuart);
+ sizableContainer.MoveTo(targetRect.Location, TRANSITION_DURATION, Easing.OutQuart);
+ sizableContainer.ResizeTo(targetRect.Size, TRANSITION_DURATION, Easing.OutQuart);
// Of note, this will not work great in the case of nested ScalingContainers where multiple are applying corner radius.
// Masking and corner radius should likely only be applied at one point in the full game stack to fix this.
// An example of how this can occur is when the skin editor is visible and the game screen scaling is set to "Everything".
- sizableContainer.TransformTo(nameof(CornerRadius), requiresMasking ? corner_radius : 0, duration, requiresMasking ? Easing.OutQuart : Easing.None)
+ sizableContainer.TransformTo(nameof(CornerRadius), requiresMasking ? corner_radius : 0, TRANSITION_DURATION, requiresMasking ? Easing.OutQuart : Easing.None)
.OnComplete(_ => { sizableContainer.Masking = requiresMasking; });
}
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/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/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/AudioSettingsStrings.cs b/osu.Game/Localisation/AudioSettingsStrings.cs
index f298717c99..0f0f560df9 100644
--- a/osu.Game/Localisation/AudioSettingsStrings.cs
+++ b/osu.Game/Localisation/AudioSettingsStrings.cs
@@ -30,12 +30,12 @@ namespace osu.Game.Localisation
public static LocalisableString OutputDevice => new TranslatableString(getKey(@"output_device"), @"Output device");
///
- /// "Master"
+ /// "Hitsound stereo separation"
///
public static LocalisableString PositionalLevel => new TranslatableString(getKey(@"positional_hitsound_audio_level"), @"Hitsound stereo separation");
///
- /// "Level"
+ /// "Master"
///
public static LocalisableString MasterVolume => new TranslatableString(getKey(@"master_volume"), @"Master");
@@ -69,6 +69,6 @@ namespace osu.Game.Localisation
///
public static LocalisableString OffsetWizard => new TranslatableString(getKey(@"offset_wizard"), @"Offset wizard");
- private static string getKey(string key) => $"{prefix}:{key}";
+ private static string getKey(string key) => $@"{prefix}:{key}";
}
}
diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
index e392ae619f..82d03dbb5b 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"
///
@@ -205,7 +210,7 @@ namespace osu.Game.Localisation
public static LocalisableString ToggleInGameInterface => new TranslatableString(getKey(@"toggle_in_game_interface"), @"Toggle in-game interface");
///
- /// "Toggle Mod Select"
+ /// "Toggle mod select"
///
public static LocalisableString ToggleModSelection => new TranslatableString(getKey(@"toggle_mod_selection"), @"Toggle mod select");
@@ -294,6 +299,6 @@ namespace osu.Game.Localisation
///
public static LocalisableString ToggleChatFocus => new TranslatableString(getKey(@"toggle_chat_focus"), @"Toggle chat focus");
- private static string getKey(string key) => $"{prefix}:{key}";
+ private static string getKey(string key) => $@"{prefix}:{key}";
}
}
diff --git a/osu.Game/Localisation/JoystickSettingsStrings.cs b/osu.Game/Localisation/JoystickSettingsStrings.cs
index 410cd0a6f5..976ec1adde 100644
--- a/osu.Game/Localisation/JoystickSettingsStrings.cs
+++ b/osu.Game/Localisation/JoystickSettingsStrings.cs
@@ -15,10 +15,10 @@ namespace osu.Game.Localisation
public static LocalisableString JoystickGamepad => new TranslatableString(getKey(@"joystick_gamepad"), @"Joystick / Gamepad");
///
- /// "Deadzone Threshold"
+ /// "Deadzone"
///
public static LocalisableString DeadzoneThreshold => new TranslatableString(getKey(@"deadzone_threshold"), @"Deadzone");
private static string getKey(string key) => $@"{prefix}:{key}";
}
-}
\ No newline at end of file
+}
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/Localisation/SkinSettingsStrings.cs b/osu.Game/Localisation/SkinSettingsStrings.cs
index 8b74b94d59..81035c5a5e 100644
--- a/osu.Game/Localisation/SkinSettingsStrings.cs
+++ b/osu.Game/Localisation/SkinSettingsStrings.cs
@@ -54,6 +54,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString ExportSkinButton => new TranslatableString(getKey(@"export_skin_button"), @"Export selected skin");
+ ///
+ /// "Delete selected skin"
+ ///
+ public static LocalisableString DeleteSkinButton => new TranslatableString(getKey(@"delete_skin_button"), @"Delete selected skin");
+
private static string getKey(string key) => $"{prefix}:{key}";
}
}
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 31f67bcecc..c96e7c4cd3 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -61,8 +61,7 @@ namespace osu.Game.Online.Chat
///
public IBindableList AvailableChannels => availableChannels;
- [Resolved]
- private IAPIProvider api { get; set; }
+ private readonly IAPIProvider api;
[Resolved]
private UserLookupCache users { get; set; }
@@ -71,8 +70,9 @@ namespace osu.Game.Online.Chat
private readonly IBindable isIdle = new BindableBool();
- public ChannelManager()
+ public ChannelManager(IAPIProvider api)
{
+ this.api = api;
CurrentChannel.ValueChanged += currentChannelChanged;
}
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/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs
index ca6082e19b..fbc5ef79ef 100644
--- a/osu.Game/Online/Chat/MessageNotifier.cs
+++ b/osu.Game/Online/Chat/MessageNotifier.cs
@@ -77,7 +77,7 @@ namespace osu.Game.Online.Chat
if (!messages.Any())
return;
- var channel = channelManager.JoinedChannels.SingleOrDefault(c => c.Id == messages.First().ChannelId);
+ var channel = channelManager.JoinedChannels.SingleOrDefault(c => c.Id > 0 && c.Id == messages.First().ChannelId);
if (channel == null)
return;
diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs
index b7e1bc999b..bbfffea6fd 100644
--- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs
+++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs
@@ -155,39 +155,42 @@ namespace osu.Game.Online.Chat
{
public Func CreateChatLineAction;
- [Resolved]
- private OsuColour colours { get; set; }
-
public StandAloneDrawableChannel(Channel channel)
: base(channel)
{
}
- [BackgroundDependencyLoader]
- private void load()
- {
- 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 => 5;
+ protected override float DateAlign => 125;
+
+ 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;
+ }
}
protected class StandAloneMessage : ChatLine
{
protected override float TextSize => 15;
-
- protected override float HorizontalPadding => 10;
- protected override float MessagePadding => 120;
- protected override float TimestampPadding => 50;
+ protected override float Spacing => 5;
+ protected override float TimestampWidth => 45;
+ protected override float UsernameWidth => 75;
public StandAloneMessage(Message message)
: base(message)
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/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/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs
index 78beda6298..9bf49364f3 100644
--- a/osu.Game/Online/Spectator/SpectatorClient.cs
+++ b/osu.Game/Online/Spectator/SpectatorClient.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Online.Spectator
///
/// The states of all users currently being watched.
///
- public IBindableDictionary WatchedUserStates => watchedUserStates;
+ public virtual IBindableDictionary WatchedUserStates => watchedUserStates;
///
/// A global list of all players currently playing.
@@ -172,6 +172,7 @@ namespace osu.Game.Online.Spectator
currentState.RulesetID = score.ScoreInfo.RulesetID;
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
currentState.State = SpectatedUserState.Playing;
+ currentState.MaximumScoringValues = state.ScoreProcessor.MaximumScoringValues;
currentBeatmap = state.Beatmap;
currentScore = score;
diff --git a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
new file mode 100644
index 0000000000..a1e8715c8f
--- /dev/null
+++ b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
@@ -0,0 +1,192 @@
+// 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 System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Timing;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+
+namespace osu.Game.Online.Spectator
+{
+ ///
+ /// A wrapper over a for spectated users.
+ /// This should be used when a local "playable" beatmap is unavailable or expensive to generate for the spectated user.
+ ///
+ public class SpectatorScoreProcessor : Component
+ {
+ ///
+ /// The current total score.
+ ///
+ public readonly BindableDouble TotalScore = new BindableDouble { MinValue = 0 };
+
+ ///
+ /// The current accuracy.
+ ///
+ public readonly BindableDouble Accuracy = new BindableDouble(1) { MinValue = 0, MaxValue = 1 };
+
+ ///
+ /// The current combo.
+ ///
+ public readonly BindableInt Combo = new BindableInt();
+
+ ///
+ /// The used to calculate scores.
+ ///
+ public readonly Bindable Mode = new Bindable();
+
+ ///
+ /// The applied s.
+ ///
+ public IReadOnlyList Mods => scoreProcessor?.Mods.Value ?? Array.Empty();
+
+ private IClock? referenceClock;
+
+ ///
+ /// The clock used to determine the current score.
+ ///
+ public IClock ReferenceClock
+ {
+ get => referenceClock ?? Clock;
+ set => referenceClock = value;
+ }
+
+ [Resolved]
+ private SpectatorClient spectatorClient { get; set; } = null!;
+
+ [Resolved]
+ private RulesetStore rulesetStore { get; set; } = null!;
+
+ private readonly IBindableDictionary spectatorStates = new BindableDictionary();
+ private readonly List replayFrames = new List();
+ private readonly int userId;
+
+ private SpectatorState? spectatorState;
+ private ScoreProcessor? scoreProcessor;
+ private ScoreInfo? scoreInfo;
+
+ public SpectatorScoreProcessor(int userId)
+ {
+ this.userId = userId;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Mode.BindValueChanged(_ => UpdateScore());
+
+ spectatorStates.BindTo(spectatorClient.WatchedUserStates);
+ spectatorStates.BindCollectionChanged(onSpectatorStatesChanged, true);
+
+ spectatorClient.OnNewFrames += onNewFrames;
+ }
+
+ private void onSpectatorStatesChanged(object? sender, NotifyDictionaryChangedEventArgs e)
+ {
+ if (!spectatorStates.TryGetValue(userId, out var userState) || userState.BeatmapID == null || userState.RulesetID == null)
+ {
+ scoreProcessor?.RemoveAndDisposeImmediately();
+ scoreProcessor = null;
+ scoreInfo = null;
+ spectatorState = null;
+ replayFrames.Clear();
+ return;
+ }
+
+ if (scoreProcessor != null)
+ return;
+
+ Debug.Assert(scoreInfo == null);
+
+ RulesetInfo? rulesetInfo = rulesetStore.GetRuleset(userState.RulesetID.Value);
+ if (rulesetInfo == null)
+ return;
+
+ Ruleset ruleset = rulesetInfo.CreateInstance();
+
+ spectatorState = userState;
+ scoreInfo = new ScoreInfo { Ruleset = rulesetInfo };
+ scoreProcessor = ruleset.CreateScoreProcessor();
+ scoreProcessor.Mods.Value = userState.Mods.Select(m => m.ToMod(ruleset)).ToArray();
+ }
+
+ private void onNewFrames(int incomingUserId, FrameDataBundle bundle)
+ {
+ if (incomingUserId != userId)
+ return;
+
+ Schedule(() =>
+ {
+ if (scoreProcessor == null)
+ return;
+
+ replayFrames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header));
+ UpdateScore();
+ });
+ }
+
+ public void UpdateScore()
+ {
+ if (scoreInfo == null || replayFrames.Count == 0)
+ return;
+
+ Debug.Assert(spectatorState != null);
+ Debug.Assert(scoreProcessor != null);
+
+ int frameIndex = replayFrames.BinarySearch(new TimedFrame(ReferenceClock.CurrentTime));
+ if (frameIndex < 0)
+ frameIndex = ~frameIndex;
+ frameIndex = Math.Clamp(frameIndex - 1, 0, replayFrames.Count - 1);
+
+ TimedFrame frame = replayFrames[frameIndex];
+ Debug.Assert(frame.Header != null);
+
+ scoreInfo.MaxCombo = frame.Header.MaxCombo;
+ scoreInfo.Statistics = frame.Header.Statistics;
+
+ Accuracy.Value = frame.Header.Accuracy;
+ Combo.Value = frame.Header.Combo;
+
+ scoreProcessor.ExtractScoringValues(frame.Header, out var currentScoringValues, out _);
+ TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, currentScoringValues, spectatorState.MaximumScoringValues);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (spectatorClient.IsNotNull())
+ spectatorClient.OnNewFrames -= onNewFrames;
+ }
+
+ private class TimedFrame : IComparable
+ {
+ public readonly double Time;
+ public readonly FrameHeader? Header;
+
+ public TimedFrame(double time)
+ {
+ Time = time;
+ }
+
+ public TimedFrame(double time, FrameHeader header)
+ {
+ Time = time;
+ Header = header;
+ }
+
+ public int CompareTo(TimedFrame other) => Time.CompareTo(other.Time);
+ }
+ }
+}
diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs
index 77686d12da..64e5f8b3a1 100644
--- a/osu.Game/Online/Spectator/SpectatorState.cs
+++ b/osu.Game/Online/Spectator/SpectatorState.cs
@@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using MessagePack;
using osu.Game.Online.API;
+using osu.Game.Scoring;
namespace osu.Game.Online.Spectator
{
@@ -27,6 +28,9 @@ namespace osu.Game.Online.Spectator
[Key(3)]
public SpectatedUserState State { get; set; }
+ [Key(4)]
+ public ScoringValues MaximumScoringValues { get; set; }
+
public bool Equals(SpectatorState other)
{
if (ReferenceEquals(null, other)) return false;
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 785881d97a..1f9a1ce938 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()
{
@@ -847,7 +851,7 @@ namespace osu.Game
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true);
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
- loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
+ loadComponentSingleFile(channelManager = new ChannelManager(API), AddInternal, true);
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
loadComponentSingleFile(new MessageNotifier(), AddInternal, true);
loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true);
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/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs
index a1d8cd5d38..56e39f212d 100644
--- a/osu.Game/Overlays/Chat/ChatLine.cs
+++ b/osu.Game/Overlays/Chat/ChatLine.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 System;
using System.Linq;
using System.Collections.Generic;
@@ -26,42 +28,6 @@ namespace osu.Game.Overlays.Chat
{
public class ChatLine : CompositeDrawable
{
- public const float LEFT_PADDING = default_message_padding + default_horizontal_padding * 2;
-
- private const float default_message_padding = 200;
-
- protected virtual float MessagePadding => default_message_padding;
-
- private const float default_timestamp_padding = 65;
-
- protected virtual float TimestampPadding => default_timestamp_padding;
-
- private const float default_horizontal_padding = 15;
-
- protected virtual float HorizontalPadding => default_horizontal_padding;
-
- protected virtual float TextSize => 20;
-
- private Color4 usernameColour;
-
- private OsuSpriteText timestamp;
-
- public ChatLine(Message message)
- {
- Message = message;
- Padding = new MarginPadding { Left = HorizontalPadding, Right = HorizontalPadding };
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
- }
-
- [Resolved(CanBeNull = true)]
- private ChannelManager chatManager { get; set; }
-
- private Message message;
- private OsuSpriteText username;
-
- public LinkFlowContainer ContentFlow { get; private set; }
-
public Message Message
{
get => message;
@@ -78,119 +44,101 @@ namespace osu.Game.Overlays.Chat
}
}
+ public LinkFlowContainer ContentFlow { get; private set; } = null!;
+
+ protected virtual float TextSize => 20;
+
+ protected virtual float Spacing => 15;
+
+ protected virtual float TimestampWidth => 60;
+
+ protected virtual float UsernameWidth => 130;
+
+ private Color4 usernameColour;
+
+ private OsuSpriteText timestamp = null!;
+
+ private Message message = null!;
+
+ private OsuSpriteText username = null!;
+
+ private Container? highlight;
+
private bool senderHasColour => !string.IsNullOrEmpty(message.Sender.Colour);
+ private bool messageHasColour => Message.IsAction && senderHasColour;
+
[Resolved]
- private OsuColour colours { get; set; }
+ private ChannelManager? chatManager { get; set; }
+
+ [Resolved]
+ private OsuColour colours { get; set; } = null!;
+
+ public ChatLine(Message message)
+ {
+ Message = message;
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ }
[BackgroundDependencyLoader]
- private void load()
+ private void load(OverlayColourProvider? colourProvider)
{
usernameColour = senderHasColour
? Color4Extensions.FromHex(message.Sender.Colour)
: username_colours[message.Sender.Id % username_colours.Length];
- Drawable effectedUsername = username = new OsuSpriteText
+ InternalChild = new GridContainer
{
- Shadow = false,
- Colour = senderHasColour ? colours.ChatBlue : usernameColour,
- Truncate = true,
- EllipsisString = "… :",
- Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Bold, italics: true),
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- MaxWidth = MessagePadding - TimestampPadding
- };
-
- if (senderHasColour)
- {
- // Background effect
- effectedUsername = new Container
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
+ ColumnDimensions = new[]
{
- AutoSizeAxes = Axes.Both,
- Masking = true,
- CornerRadius = 4,
- EdgeEffect = new EdgeEffectParameters
- {
- Roundness = 1,
- Radius = 1,
- Colour = Color4.Black.Opacity(0.3f),
- Offset = new Vector2(0, 1),
- Type = EdgeEffectType.Shadow,
- },
- Child = new Container
- {
- AutoSizeAxes = Axes.Both,
- Y = 0,
- Masking = true,
- CornerRadius = 4,
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = usernameColour,
- },
- new Container
- {
- AutoSizeAxes = Axes.Both,
- Padding = new MarginPadding { Left = 4, Right = 4, Bottom = 1, Top = -2 },
- Child = username
- }
- }
- }
- };
- }
-
- InternalChildren = new Drawable[]
- {
- new Container
- {
- Size = new Vector2(MessagePadding, TextSize),
- Children = new Drawable[]
- {
- timestamp = new OsuSpriteText
- {
- Shadow = false,
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Font = OsuFont.GetFont(size: TextSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true)
- },
- new MessageSender(message.Sender)
- {
- AutoSizeAxes = Axes.Both,
- Origin = Anchor.TopRight,
- Anchor = Anchor.TopRight,
- Child = effectedUsername,
- },
- }
+ new Dimension(GridSizeMode.Absolute, TimestampWidth + Spacing + UsernameWidth + Spacing),
+ new Dimension(),
},
- new Container
+ Content = new[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding { Left = MessagePadding + HorizontalPadding },
- Children = new Drawable[]
+ new Drawable[]
{
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
+ {
+ timestamp = new OsuSpriteText
+ {
+ Shadow = false,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Font = OsuFont.GetFont(size: TextSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true),
+ MaxWidth = TimestampWidth,
+ Colour = colourProvider?.Background1 ?? Colour4.White,
+ },
+ new MessageSender(message.Sender)
+ {
+ Width = UsernameWidth,
+ AutoSizeAxes = Axes.Y,
+ Origin = Anchor.TopRight,
+ Anchor = Anchor.TopRight,
+ Child = createUsername(),
+ Margin = new MarginPadding { Horizontal = Spacing },
+ },
+ },
+ },
ContentFlow = new LinkFlowContainer(t =>
{
t.Shadow = false;
-
- if (Message.IsAction)
- {
- t.Font = OsuFont.GetFont(italics: true);
-
- if (senderHasColour)
- t.Colour = Color4Extensions.FromHex(message.Sender.Colour);
- }
-
- t.Font = t.Font.With(size: TextSize);
+ t.Font = t.Font.With(size: TextSize, italics: Message.IsAction);
+ t.Colour = messageHasColour ? Color4Extensions.FromHex(message.Sender.Colour) : colourProvider?.Content1 ?? Colour4.White;
})
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
}
- }
+ },
}
};
}
@@ -203,8 +151,6 @@ namespace osu.Game.Overlays.Chat
FinishTransforms(true);
}
- private Container highlight;
-
///
/// Performs a highlight animation on this .
///
@@ -233,7 +179,7 @@ namespace osu.Game.Overlays.Chat
timestamp.FadeTo(message is LocalEchoMessage ? 0 : 1, 500, Easing.OutQuint);
timestamp.Text = $@"{message.Timestamp.LocalDateTime:HH:mm:ss}";
- username.Text = $@"{message.Sender.Username}" + (senderHasColour || message.IsAction ? "" : ":");
+ username.Text = $@"{message.Sender.Username}";
// remove non-existent channels from the link list
message.Links.RemoveAll(link => link.Action == LinkAction.OpenChannel && chatManager?.AvailableChannels.Any(c => c.Name == link.Argument.ToString()) != true);
@@ -242,22 +188,78 @@ namespace osu.Game.Overlays.Chat
ContentFlow.AddLinks(message.DisplayContent, message.Links);
}
+ private Drawable createUsername()
+ {
+ username = new OsuSpriteText
+ {
+ Shadow = false,
+ Colour = senderHasColour ? colours.ChatBlue : usernameColour,
+ Truncate = true,
+ EllipsisString = "…",
+ Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Bold, italics: true),
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ MaxWidth = UsernameWidth,
+ };
+
+ if (!senderHasColour)
+ return username;
+
+ // Background effect
+ return new Container
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ AutoSizeAxes = Axes.Both,
+ Masking = true,
+ CornerRadius = 4,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Roundness = 1,
+ Radius = 1,
+ Colour = Color4.Black.Opacity(0.3f),
+ Offset = new Vector2(0, 1),
+ Type = EdgeEffectType.Shadow,
+ },
+ Child = new Container
+ {
+ AutoSizeAxes = Axes.Both,
+ Masking = true,
+ CornerRadius = 4,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = usernameColour,
+ },
+ new Container
+ {
+ AutoSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Left = 4, Right = 4, Bottom = 1, Top = -2 },
+ Child = username
+ }
+ }
+ }
+ };
+ }
+
private class MessageSender : OsuClickableContainer, IHasContextMenu
{
private readonly APIUser sender;
- private Action startChatAction;
+ private Action startChatAction = null!;
[Resolved]
- private IAPIProvider api { get; set; }
+ private IAPIProvider api { get; set; } = null!;
public MessageSender(APIUser sender)
{
this.sender = sender;
}
- [BackgroundDependencyLoader(true)]
- private void load(UserProfileOverlay profile, ChannelManager chatManager)
+ [BackgroundDependencyLoader]
+ private void load(UserProfileOverlay? profile, ChannelManager? chatManager)
{
Action = () => profile?.ShowUser(sender);
startChatAction = () => chatManager?.OpenPrivateChannel(sender);
diff --git a/osu.Game/Overlays/Chat/ChatOverlayDrawableChannel.cs b/osu.Game/Overlays/Chat/ChatOverlayDrawableChannel.cs
deleted file mode 100644
index 3b47adc4b7..0000000000
--- a/osu.Game/Overlays/Chat/ChatOverlayDrawableChannel.cs
+++ /dev/null
@@ -1,109 +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;
-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
-{
- public class ChatOverlayDrawableChannel : DrawableChannel
- {
- public ChatOverlayDrawableChannel(Channel channel)
- : base(channel)
- {
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- ChatLineFlow.Padding = new MarginPadding(0);
- }
-
- protected override Drawable CreateDaySeparator(DateTimeOffset time) => new ChatOverlayDaySeparator(time);
-
- private class ChatOverlayDaySeparator : Container
- {
- private readonly DateTimeOffset time;
-
- public ChatOverlayDaySeparator(DateTimeOffset 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,
- },
- },
- },
- };
- }
- }
- }
-}
diff --git a/osu.Game/Overlays/Chat/DaySeparator.cs b/osu.Game/Overlays/Chat/DaySeparator.cs
new file mode 100644
index 0000000000..9ae35b0c38
--- /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 => 205;
+
+ 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..c3a341bca4 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 = 10 },
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/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs
index 02769b5d68..f04bf76c18 100644
--- a/osu.Game/Overlays/ChatOverlay.cs
+++ b/osu.Game/Overlays/ChatOverlay.cs
@@ -38,9 +38,9 @@ namespace osu.Game.Overlays
private LoadingLayer loading = null!;
private ChannelListing channelListing = null!;
private ChatTextBar textBar = null!;
- private Container currentChannelContainer = null!;
+ private Container currentChannelContainer = null!;
- private readonly Dictionary loadedChannels = new Dictionary();
+ private readonly Dictionary loadedChannels = new Dictionary();
protected IEnumerable DrawableChannels => loadedChannels.Values;
@@ -126,7 +126,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
- currentChannelContainer = new Container
+ currentChannelContainer = new Container
{
RelativeSizeAxes = Axes.Both,
},
@@ -313,7 +313,7 @@ namespace osu.Game.Overlays
loading.Show();
// Ensure the drawable channel is stored before async load to prevent double loading
- ChatOverlayDrawableChannel drawableChannel = CreateDrawableChannel(newChannel);
+ DrawableChannel drawableChannel = CreateDrawableChannel(newChannel);
loadedChannels.Add(newChannel, drawableChannel);
LoadComponentAsync(drawableChannel, loadedDrawable =>
@@ -338,7 +338,7 @@ namespace osu.Game.Overlays
channelManager.MarkChannelAsRead(newChannel);
}
- protected virtual ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) => new ChatOverlayDrawableChannel(newChannel);
+ protected virtual DrawableChannel CreateDrawableChannel(Channel newChannel) => new DrawableChannel(newChannel);
private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
{
@@ -361,7 +361,7 @@ namespace osu.Game.Overlays
if (loadedChannels.ContainsKey(channel))
{
- ChatOverlayDrawableChannel loaded = loadedChannels[channel];
+ DrawableChannel loaded = loadedChannels[channel];
loadedChannels.Remove(channel);
// DrawableChannel removed from cache must be manually disposed
loaded.Dispose();
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/FirstRunSetup/ScreenUIScale.cs b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs
index 8452691bb5..f09a26a527 100644
--- a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs
+++ b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs
@@ -126,6 +126,7 @@ namespace osu.Game.Overlays.FirstRunSetup
private class SampleScreenContainer : CompositeDrawable
{
private readonly OsuScreen screen;
+
// Minimal isolation from main game.
[Cached]
@@ -151,6 +152,9 @@ namespace osu.Game.Overlays.FirstRunSetup
RelativeSizeAxes = Axes.Both;
}
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
+ new DependencyContainer(new DependencyIsolationContainer(base.CreateChildDependencies(parent)));
+
[BackgroundDependencyLoader]
private void load(AudioManager audio, TextureStore textures, RulesetStore rulesets)
{
@@ -197,5 +201,41 @@ namespace osu.Game.Overlays.FirstRunSetup
stack.PushSynchronously(screen);
}
}
+
+ private class DependencyIsolationContainer : IReadOnlyDependencyContainer
+ {
+ private readonly IReadOnlyDependencyContainer parentDependencies;
+
+ private readonly Type[] isolatedTypes =
+ {
+ typeof(OsuGame)
+ };
+
+ public DependencyIsolationContainer(IReadOnlyDependencyContainer parentDependencies)
+ {
+ this.parentDependencies = parentDependencies;
+ }
+
+ public object Get(Type type)
+ {
+ if (isolatedTypes.Contains(type))
+ return null;
+
+ return parentDependencies.Get(type);
+ }
+
+ public object Get(Type type, CacheInfo info)
+ {
+ if (isolatedTypes.Contains(type))
+ return null;
+
+ return parentDependencies.Get(type, info);
+ }
+
+ public void Inject(T instance) where T : class
+ {
+ parentDependencies.Inject(instance);
+ }
+ }
}
}
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/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 a87e65b735..b83600a16d 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -16,6 +16,7 @@ using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
+using osu.Game.Screens.Select;
using osu.Game.Skinning;
using osu.Game.Skinning.Editor;
using Realms;
@@ -67,6 +68,7 @@ namespace osu.Game.Overlays.Settings.Sections
Action = () => skinEditor?.ToggleVisibility(),
},
new ExportSkinButton(),
+ new DeleteSkinButton(),
};
config.BindWith(OsuSetting.Skin, configBindable);
@@ -202,5 +204,36 @@ namespace osu.Game.Overlays.Settings.Sections
}
}
}
+
+ public class DeleteSkinButton : DangerousSettingsButton
+ {
+ [Resolved]
+ private SkinManager skins { get; set; }
+
+ [Resolved(CanBeNull = true)]
+ private IDialogOverlay dialogOverlay { get; set; }
+
+ private Bindable currentSkin;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Text = SkinSettingsStrings.DeleteSkinButton;
+ Action = delete;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ currentSkin = skins.CurrentSkin.GetBoundCopy();
+ currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.PerformRead(s => !s.Protected), true);
+ }
+
+ private void delete()
+ {
+ dialogOverlay?.Push(new SkinDeleteDialog(currentSkin.Value));
+ }
+ }
}
}
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/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 929c362bd8..24d9f785f2 100644
--- a/osu.Game/Overlays/Volume/VolumeMeter.cs
+++ b/osu.Game/Overlays/Volume/VolumeMeter.cs
@@ -15,7 +15,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
-using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Framework.Utils;
@@ -28,7 +27,7 @@ using osuTK.Graphics;
namespace osu.Game.Overlays.Volume
{
- public class VolumeMeter : Container, IKeyBindingHandler, IStateful
+ public class VolumeMeter : Container, IStateful
{
private CircularProgress volumeCircle;
private CircularProgress volumeCircleGlow;
@@ -80,7 +79,7 @@ namespace osu.Game.Overlays.Volume
[BackgroundDependencyLoader]
private void load(OsuColour colours, AudioManager audio)
{
- hoverSample = audio.Samples.Get($"UI/{HoverSampleSet.Button.GetDescription()}-hover");
+ hoverSample = audio.Samples.Get($@"UI/{HoverSampleSet.Button.GetDescription()}-hover");
notchSample = audio.Samples.Get(@"UI/notch-tick");
sampleLastPlaybackTime = Time.Current;
@@ -132,7 +131,7 @@ namespace osu.Game.Overlays.Volume
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Name = "Progress under covers for smoothing",
+ Name = @"Progress under covers for smoothing",
RelativeSizeAxes = Axes.Both,
Rotation = 180,
Child = volumeCircle = new CircularProgress
@@ -144,7 +143,7 @@ namespace osu.Game.Overlays.Volume
},
new Circle
{
- Name = "Inner Cover",
+ Name = @"Inner Cover",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
@@ -153,7 +152,7 @@ namespace osu.Game.Overlays.Volume
},
new Container
{
- Name = "Progress overlay for glow",
+ Name = @"Progress overlay for glow",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
@@ -365,27 +364,6 @@ namespace osu.Game.Overlays.Volume
{
}
- public bool OnPressed(KeyBindingPressEvent e)
- {
- if (!IsHovered)
- return false;
-
- switch (e.Action)
- {
- case GlobalAction.SelectPreviousGroup:
- State = SelectionState.Selected;
- adjust(1, false);
- return true;
-
- case GlobalAction.SelectNextGroup:
- State = SelectionState.Selected;
- adjust(-1, false);
- return true;
- }
-
- return false;
- }
-
public void OnReleased(KeyBindingReleaseEvent e)
{
}
diff --git a/osu.Game/Overlays/VolumeOverlay.cs b/osu.Game/Overlays/VolumeOverlay.cs
index a96949e96f..9d2ed3f837 100644
--- a/osu.Game/Overlays/VolumeOverlay.cs
+++ b/osu.Game/Overlays/VolumeOverlay.cs
@@ -17,6 +17,7 @@ using osu.Game.Input.Bindings;
using osu.Game.Overlays.Volume;
using osuTK;
using osuTK.Graphics;
+using osuTK.Input;
namespace osu.Game.Overlays
{
@@ -171,6 +172,30 @@ namespace osu.Game.Overlays
return base.OnMouseMove(e);
}
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ switch (e.Key)
+ {
+ case Key.Left:
+ Adjust(GlobalAction.PreviousVolumeMeter);
+ return true;
+
+ case Key.Right:
+ Adjust(GlobalAction.NextVolumeMeter);
+ return true;
+
+ case Key.Down:
+ Adjust(GlobalAction.DecreaseVolume);
+ return true;
+
+ case Key.Up:
+ Adjust(GlobalAction.IncreaseVolume);
+ return true;
+ }
+
+ return base.OnKeyDown(e);
+ }
+
protected override bool OnHover(HoverEvent e)
{
schedulePopOut();
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/BottomBar.cs b/osu.Game/Screens/Edit/BottomBar.cs
index 5994184c11..62caaced89 100644
--- a/osu.Game/Screens/Edit/BottomBar.cs
+++ b/osu.Game/Screens/Edit/BottomBar.cs
@@ -2,13 +2,16 @@
// 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
{
@@ -26,6 +29,14 @@ namespace osu.Game.Screens.Edit
Height = 60;
+ Masking = true;
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Colour = Color4.Black.Opacity(0.2f),
+ Type = EdgeEffectType.Shadow,
+ Radius = 10f,
+ };
+
InternalChildren = new Drawable[]
{
new Box
diff --git a/osu.Game/Screens/Edit/Components/EditorSidebar.cs b/osu.Game/Screens/Edit/Components/EditorSidebar.cs
index 4edcef41b1..4e9b1d5222 100644
--- a/osu.Game/Screens/Edit/Components/EditorSidebar.cs
+++ b/osu.Game/Screens/Edit/Components/EditorSidebar.cs
@@ -16,13 +16,15 @@ namespace osu.Game.Screens.Edit.Components
///
internal class EditorSidebar : Container
{
+ public const float WIDTH = 250;
+
private readonly Box background;
protected override Container Content { get; }
public EditorSidebar()
{
- Width = 250;
+ Width = WIDTH;
RelativeSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineButton.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineButton.cs
index 5550c6a748..e0b21b2e22 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineButton.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineButton.cs
@@ -1,99 +1,29 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
-using 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.Threading;
-using osu.Game.Graphics;
+using osu.Framework.Allocation;
using osu.Game.Graphics.UserInterface;
-using osuTK;
-using osuTK.Graphics;
-using osuTK.Input;
+using osu.Game.Overlays;
+using osu.Game.Screens.Edit.Timing;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
- public class TimelineButton : CompositeDrawable
+ public class TimelineButton : IconButton
{
- public Action Action;
- public readonly BindableBool Enabled = new BindableBool(true);
-
- public IconUsage Icon
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
{
- get => button.Icon;
- set => button.Icon = value;
+ // These are using colourProvider but don't match the design.
+ // Just something to fit until someone implements the updated design.
+ IconColour = colourProvider.Background1;
+ IconHoverColour = colourProvider.Content2;
+
+ HoverColour = colourProvider.Background1;
+ FlashColour = colourProvider.Content2;
+
+ Add(new RepeatingButtonBehaviour(this));
}
- private readonly IconButton button;
-
- public TimelineButton()
- {
- InternalChild = button = new TimelineIconButton { Action = () => Action?.Invoke() };
-
- button.Enabled.BindTo(Enabled);
- Width = button.Width;
- }
-
- protected override void Update()
- {
- base.Update();
-
- button.Size = new Vector2(button.Width, DrawHeight);
- }
-
- private class TimelineIconButton : IconButton
- {
- public TimelineIconButton()
- {
- Anchor = Anchor.Centre;
- Origin = Anchor.Centre;
- IconColour = OsuColour.Gray(0.35f);
- IconHoverColour = Color4.White;
- HoverColour = OsuColour.Gray(0.25f);
- FlashColour = OsuColour.Gray(0.5f);
- }
-
- private ScheduledDelegate repeatSchedule;
-
- ///
- /// The initial delay before mouse down repeat begins.
- ///
- private const int repeat_initial_delay = 250;
-
- ///
- /// The delay between mouse down repeats after the initial repeat.
- ///
- private const int repeat_tick_rate = 70;
-
- protected override bool OnClick(ClickEvent e)
- {
- // don't actuate a click since we are manually handling repeats.
- return true;
- }
-
- protected override bool OnMouseDown(MouseDownEvent e)
- {
- if (e.Button == MouseButton.Left)
- {
- Action clickAction = () => base.OnClick(new ClickEvent(e.CurrentState, e.Button));
-
- // run once for initial down
- clickAction();
-
- Scheduler.Add(repeatSchedule = new ScheduledDelegate(clickAction, Clock.CurrentTime + repeat_initial_delay, repeat_tick_rate));
- }
-
- return base.OnMouseDown(e);
- }
-
- protected override void OnMouseUp(MouseUpEvent e)
- {
- repeatSchedule?.Cancel();
- base.OnMouseUp(e);
- }
- }
+ protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
index 35d103ddf1..d008368b69 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Input.Events;
+using osu.Framework.Layout;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
@@ -40,10 +41,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved(canBeNull: true)]
private IFrameBasedClock editorClock { get; set; }
+ private readonly LayoutValue zoomedContentWidthCache = new LayoutValue(Invalidation.DrawSize);
+
public ZoomableScrollContainer()
: base(Direction.Horizontal)
{
base.Content.Add(zoomedContent = new Container { RelativeSizeAxes = Axes.Y });
+
+ AddLayout(zoomedContentWidthCache);
}
private float minZoom = 1;
@@ -103,12 +108,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
- protected override void LoadComplete()
+ protected override void Update()
{
- base.LoadComplete();
+ base.Update();
- // This width only gets updated on the application of a transform, so this needs to be initialized here.
- updateZoomedContentWidth();
+ if (!zoomedContentWidthCache.IsValid)
+ updateZoomedContentWidth();
}
protected override bool OnScroll(ScrollEvent e)
@@ -128,7 +133,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return base.OnScroll(e);
}
- private void updateZoomedContentWidth() => zoomedContent.Width = DrawWidth * currentZoom;
+ private void updateZoomedContentWidth()
+ {
+ zoomedContent.Width = DrawWidth * currentZoom;
+ zoomedContentWidthCache.Validate();
+ }
private float zoomTarget = 1;
@@ -199,8 +208,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset;
d.currentZoom = newZoom;
-
d.updateZoomedContentWidth();
+
// Temporarily here to make sure ScrollTo gets the correct DrawSize for scrollable area.
// TODO: Make sure draw size gets invalidated properly on the framework side, and remove this once it is.
d.Invalidate(Invalidation.DrawSize);
diff --git a/osu.Game/Screens/Edit/EditorTable.cs b/osu.Game/Screens/Edit/EditorTable.cs
index a67a060134..26819dcfe7 100644
--- a/osu.Game/Screens/Edit/EditorTable.cs
+++ b/osu.Game/Screens/Edit/EditorTable.cs
@@ -99,6 +99,15 @@ namespace osu.Game.Screens.Edit
colourSelected = colours.Colour3;
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ // Reduce flicker of rows when offset is being changed rapidly.
+ // Probably need to reconsider this.
+ FinishTransforms(true);
+ }
+
private bool selected;
public bool Selected
diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs
index aae19396db..fd916894ea 100644
--- a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs
+++ b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.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 System;
using System.Collections.Generic;
using System.IO;
@@ -9,6 +11,7 @@ using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
@@ -27,10 +30,10 @@ namespace osu.Game.Screens.Edit.Setup
public IEnumerable HandledExtensions => handledExtensions;
- private readonly Bindable currentFile = new Bindable();
+ private readonly Bindable currentFile = new Bindable();
[Resolved]
- private OsuGameBase game { get; set; }
+ private OsuGameBase game { get; set; } = null!;
public FileChooserLabelledTextBox(params string[] handledExtensions)
{
@@ -45,7 +48,7 @@ namespace osu.Game.Screens.Edit.Setup
currentFile.BindValueChanged(onFileSelected);
}
- private void onFileSelected(ValueChangedEvent file)
+ private void onFileSelected(ValueChangedEvent file)
{
if (file.NewValue == null)
return;
@@ -65,14 +68,16 @@ namespace osu.Game.Screens.Edit.Setup
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
- game.UnregisterImportHandler(this);
+
+ if (game.IsNotNull())
+ game.UnregisterImportHandler(this);
}
public override Popover GetPopover() => new FileChooserPopover(handledExtensions, currentFile);
private class FileChooserPopover : OsuPopover
{
- public FileChooserPopover(string[] handledExtensions, Bindable currentFile)
+ public FileChooserPopover(string[] handledExtensions, Bindable currentFile)
{
Child = new Container
{
diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs
index 0c12eff503..77d875b67f 100644
--- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs
+++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs
@@ -61,6 +61,7 @@ namespace osu.Game.Screens.Edit.Timing
selectedGroup.BindValueChanged(group =>
{
+ // TODO: This should scroll the selected row into view.
foreach (var b in BackgroundFlow) b.Selected = b.Item == group.NewValue;
}, true);
}
diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs
index c8944d0357..c9f73411f1 100644
--- a/osu.Game/Screens/Edit/Timing/EffectSection.cs
+++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs
@@ -31,18 +31,33 @@ namespace osu.Game.Screens.Edit.Timing
});
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ kiai.Current.BindValueChanged(_ => saveChanges());
+ omitBarLine.Current.BindValueChanged(_ => saveChanges());
+ scrollSpeedSlider.Current.BindValueChanged(_ => saveChanges());
+
+ void saveChanges()
+ {
+ if (!isRebinding) ChangeHandler?.SaveState();
+ }
+ }
+
+ private bool isRebinding;
+
protected override void OnControlPointChanged(ValueChangedEvent point)
{
if (point.NewValue != null)
{
+ isRebinding = true;
+
kiai.Current = point.NewValue.KiaiModeBindable;
- kiai.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
-
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
- omitBarLine.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
-
scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable;
- scrollSpeedSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
+
+ isRebinding = false;
}
}
diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs
index bb2dd35a9c..f613488aae 100644
--- a/osu.Game/Screens/Edit/Timing/GroupSection.cs
+++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Screens.Edit.Timing
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
- Padding = new MarginPadding(10);
+ Padding = new MarginPadding(10) { Bottom = 0 };
InternalChildren = new Drawable[]
{
diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs
index 57fcff6a4c..2ecd66a05f 100644
--- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs
+++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs
@@ -3,6 +3,8 @@
using System;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
@@ -31,12 +33,18 @@ namespace osu.Game.Screens.Edit.Timing
private IAdjustableClock metronomeClock;
+ private Sample clunk;
+
[Resolved]
private OverlayColourProvider overlayColourProvider { get; set; }
+ public bool EnableClicking { get; set; } = true;
+
[BackgroundDependencyLoader]
- private void load()
+ private void load(AudioManager audio)
{
+ clunk = audio.Samples.Get(@"Multiplayer/countdown-tick");
+
const float taper = 25;
const float swing_vertical_offset = -23;
const float lower_cover_height = 32;
@@ -269,8 +277,24 @@ namespace osu.Game.Screens.Edit.Timing
if (currentAngle != 0 && Math.Abs(currentAngle - targetAngle) > angle * 1.8f && isSwinging)
{
- using (stick.BeginDelayedSequence(beatLength / 2))
+ using (BeginDelayedSequence(beatLength / 2))
+ {
stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint);
+
+ Schedule(() =>
+ {
+ if (!EnableClicking)
+ return;
+
+ var channel = clunk?.GetChannel();
+
+ if (channel != null)
+ {
+ channel.Frequency.Value = RNG.NextDouble(0.98f, 1.02f);
+ channel.Play();
+ }
+ });
+ }
}
}
}
diff --git a/osu.Game/Screens/Edit/Timing/RepeatingButtonBehaviour.cs b/osu.Game/Screens/Edit/Timing/RepeatingButtonBehaviour.cs
new file mode 100644
index 0000000000..595305b20f
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/RepeatingButtonBehaviour.cs
@@ -0,0 +1,92 @@
+// 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.Graphics;
+using osu.Framework.Input.Events;
+using osu.Framework.Threading;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ ///
+ /// Represents a component that provides the behaviour of triggering button clicks repeatedly while holding with mouse.
+ ///
+ public class RepeatingButtonBehaviour : Component
+ {
+ private const double initial_delay = 300;
+ private const double minimum_delay = 80;
+
+ private readonly Drawable button;
+
+ private Sample sample;
+
+ ///
+ /// An additive modifier for the frequency of the sample played on next actuation.
+ /// This can be adjusted during the button's event to affect the repeat sample playback of that click.
+ ///
+ public double SampleFrequencyModifier { get; set; }
+
+ public RepeatingButtonBehaviour(Drawable button)
+ {
+ this.button = button;
+
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(AudioManager audio)
+ {
+ sample = audio.Samples.Get(@"UI/notch-tick");
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ beginRepeat();
+ return true;
+ }
+
+ protected override void OnMouseUp(MouseUpEvent e)
+ {
+ adjustDelegate?.Cancel();
+ base.OnMouseUp(e);
+ }
+
+ private ScheduledDelegate adjustDelegate;
+ private double adjustDelay = initial_delay;
+
+ private void beginRepeat()
+ {
+ adjustDelegate?.Cancel();
+
+ adjustDelay = initial_delay;
+ adjustNext();
+
+ void adjustNext()
+ {
+ if (IsHovered)
+ {
+ button.TriggerClick();
+ adjustDelay = Math.Max(minimum_delay, adjustDelay * 0.9f);
+
+ var channel = sample?.GetChannel();
+
+ if (channel != null)
+ {
+ double repeatModifier = 0.05f * (Math.Abs(adjustDelay - initial_delay) / minimum_delay);
+ channel.Frequency.Value = 1 + repeatModifier + SampleFrequencyModifier;
+ channel.Play();
+ }
+ }
+ else
+ {
+ adjustDelay = initial_delay;
+ }
+
+ adjustDelegate = Scheduler.AddDelayed(adjustNext, adjustDelay);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/Section.cs b/osu.Game/Screens/Edit/Timing/Section.cs
index 139abfb187..17147c21f4 100644
--- a/osu.Game/Screens/Edit/Timing/Section.cs
+++ b/osu.Game/Screens/Edit/Timing/Section.cs
@@ -77,7 +77,7 @@ namespace osu.Game.Screens.Edit.Timing
{
Flow = new FillFlowContainer
{
- Padding = new MarginPadding(20),
+ Padding = new MarginPadding(10) { Top = 0 },
Spacing = new Vector2(20),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
diff --git a/osu.Game/Screens/Edit/Timing/TapButton.cs b/osu.Game/Screens/Edit/Timing/TapButton.cs
new file mode 100644
index 0000000000..a6227cbe27
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/TapButton.cs
@@ -0,0 +1,418 @@
+// 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 System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Framework.Threading;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Input.Bindings;
+using osu.Game.Overlays;
+using osuTK;
+using osuTK.Graphics;
+using osuTK.Input;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ internal class TapButton : CircularContainer, IKeyBindingHandler
+ {
+ public const float SIZE = 140;
+
+ public readonly BindableBool IsHandlingTapping = new BindableBool();
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ [Resolved(canBeNull: true)]
+ private Bindable? selectedGroup { get; set; }
+
+ [Resolved(canBeNull: true)]
+ private IBeatSyncProvider? beatSyncSource { get; set; }
+
+ private Circle hoverLayer = null!;
+
+ private CircularContainer innerCircle = null!;
+ private Box innerCircleHighlight = null!;
+
+ private int currentLight;
+
+ private Container scaleContainer = null!;
+ private Container lights = null!;
+ private Container lightsGlow = null!;
+ private OsuSpriteText bpmText = null!;
+ private Container textContainer = null!;
+
+ private bool grabbedMouseDown;
+
+ private ScheduledDelegate? resetDelegate;
+
+ private const int light_count = 8;
+
+ private const int initial_taps_to_ignore = 4;
+
+ private const int max_taps_to_consider = 128;
+
+ private const double transition_length = 500;
+
+ private const float angular_light_gap = 0.007f;
+
+ private readonly List tapTimings = new List();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Size = new Vector2(SIZE);
+
+ const float ring_width = 22;
+ const float light_padding = 3;
+
+ InternalChild = scaleContainer = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Circle
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background4
+ },
+ lights = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ new CircularContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Name = @"outer masking",
+ Masking = true,
+ BorderThickness = light_padding,
+ BorderColour = colourProvider.Background4,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = Color4.Black,
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true,
+ },
+ }
+ },
+ new Circle
+ {
+ Name = @"inner masking",
+ Size = new Vector2(SIZE - ring_width * 2 + light_padding * 2),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = colourProvider.Background4,
+ },
+ lightsGlow = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ innerCircle = new CircularContainer
+ {
+ Size = new Vector2(SIZE - ring_width * 2),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Masking = true,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colourProvider.Background2,
+ RelativeSizeAxes = Axes.Both,
+ },
+ innerCircleHighlight = new Box
+ {
+ Colour = colourProvider.Colour3,
+ Blending = BlendingParameters.Additive,
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ },
+ textContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background1,
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Font = OsuFont.Torus.With(size: 34, weight: FontWeight.SemiBold),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.BottomCentre,
+ Y = 5,
+ Text = "Tap",
+ },
+ bpmText = new OsuSpriteText
+ {
+ Font = OsuFont.Torus.With(size: 23, weight: FontWeight.Regular),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.TopCentre,
+ Y = -1,
+ },
+ }
+ },
+ hoverLayer = new Circle
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background1.Opacity(0.3f),
+ Blending = BlendingParameters.Additive,
+ Alpha = 0,
+ },
+ }
+ },
+ }
+ };
+
+ for (int i = 0; i < light_count; i++)
+ {
+ var light = new Light
+ {
+ Rotation = (i + 1) * (360f / light_count) + 360 * angular_light_gap / 2,
+ };
+
+ lights.Add(light);
+ lightsGlow.Add(light.Glow.CreateProxy());
+ }
+
+ reset();
+ }
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
+ hoverLayer.ReceivePositionalInputAt(screenSpacePos);
+
+ private ColourInfo textColour
+ {
+ get
+ {
+ if (grabbedMouseDown)
+ return colourProvider.Background4;
+
+ if (IsHovered)
+ return colourProvider.Content2;
+
+ return colourProvider.Background1;
+ }
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ hoverLayer.FadeIn(transition_length, Easing.OutQuint);
+ textContainer.FadeColour(textColour, transition_length, Easing.OutQuint);
+ return true;
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ hoverLayer.FadeOut(transition_length, Easing.OutQuint);
+ textContainer.FadeColour(textColour, transition_length, Easing.OutQuint);
+ base.OnHoverLost(e);
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ const double in_duration = 100;
+
+ grabbedMouseDown = true;
+ IsHandlingTapping.Value = true;
+
+ resetDelegate?.Cancel();
+
+ handleTap();
+
+ textContainer.FadeColour(textColour, in_duration, Easing.OutQuint);
+
+ scaleContainer.ScaleTo(0.99f, in_duration, Easing.OutQuint);
+ innerCircle.ScaleTo(0.96f, in_duration, Easing.OutQuint);
+
+ innerCircleHighlight
+ .FadeIn(50, Easing.OutQuint)
+ .FlashColour(Color4.White, 1000, Easing.OutQuint);
+
+ lights[currentLight % light_count].Hide();
+ lights[(currentLight + light_count / 2) % light_count].Hide();
+
+ currentLight++;
+
+ lights[currentLight % light_count].Show();
+ lights[(currentLight + light_count / 2) % light_count].Show();
+
+ return true;
+ }
+
+ protected override void OnMouseUp(MouseUpEvent e)
+ {
+ const double out_duration = 800;
+
+ grabbedMouseDown = false;
+
+ textContainer.FadeColour(textColour, out_duration, Easing.OutQuint);
+
+ scaleContainer.ScaleTo(1, out_duration, Easing.OutQuint);
+ innerCircle.ScaleTo(1, out_duration, Easing.OutQuint);
+
+ innerCircleHighlight.FadeOut(out_duration, Easing.OutQuint);
+
+ resetDelegate = Scheduler.AddDelayed(reset, 1000);
+
+ base.OnMouseUp(e);
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ if (e.Action == GlobalAction.EditorTapForBPM && !e.Repeat)
+ {
+ // Direct through mouse handling to achieve animation
+ OnMouseDown(new MouseDownEvent(e.CurrentState, MouseButton.Left));
+ return true;
+ }
+
+ return false;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ if (e.Action == GlobalAction.EditorTapForBPM)
+ OnMouseUp(new MouseUpEvent(e.CurrentState, MouseButton.Left));
+ }
+
+ private void handleTap()
+ {
+ tapTimings.Add(Clock.CurrentTime);
+
+ if (tapTimings.Count > initial_taps_to_ignore + max_taps_to_consider)
+ tapTimings.RemoveAt(0);
+
+ if (tapTimings.Count < initial_taps_to_ignore * 2)
+ {
+ bpmText.Text = new string('.', tapTimings.Count);
+ return;
+ }
+
+ double averageBeatLength = (tapTimings.Last() - tapTimings.Skip(initial_taps_to_ignore).First()) / (tapTimings.Count - initial_taps_to_ignore - 1);
+ double clockRate = beatSyncSource?.Clock?.Rate ?? 1;
+
+ double bpm = Math.Round(60000 / averageBeatLength / clockRate);
+
+ bpmText.Text = $"{bpm} BPM";
+
+ var timingPoint = selectedGroup?.Value.ControlPoints.OfType().FirstOrDefault();
+
+ if (timingPoint != null)
+ {
+ // Intentionally use the rounded BPM here.
+ timingPoint.BeatLength = 60000 / bpm;
+ }
+ }
+
+ private void reset()
+ {
+ bpmText.FadeOut(transition_length, Easing.OutQuint);
+
+ using (BeginDelayedSequence(tapTimings.Count > 0 ? transition_length : 0))
+ {
+ Schedule(() => bpmText.Text = "the beat!");
+ bpmText.FadeIn(800, Easing.OutQuint);
+ }
+
+ foreach (var light in lights)
+ light.Hide();
+
+ tapTimings.Clear();
+ currentLight = 0;
+ IsHandlingTapping.Value = false;
+ }
+
+ private class Light : CompositeDrawable
+ {
+ public Drawable Glow { get; private set; } = null!;
+
+ private Container fillContent = null!;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.Both;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Size = new Vector2(0.98f); // Avoid bleed into masking edge.
+
+ InternalChildren = new Drawable[]
+ {
+ new CircularProgress
+ {
+ RelativeSizeAxes = Axes.Both,
+ Current = { Value = 1f / light_count - angular_light_gap },
+ Colour = colourProvider.Background2,
+ },
+ fillContent = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ Colour = colourProvider.Colour1,
+ Children = new[]
+ {
+ new CircularProgress
+ {
+ RelativeSizeAxes = Axes.Both,
+ Current = { Value = 1f / light_count - angular_light_gap },
+ Blending = BlendingParameters.Additive
+ },
+ // Please do not try and make sense of this.
+ // Getting the visual effect I was going for relies on what I can only imagine is broken implementation
+ // of `PadExtent`. If that's ever fixed in the future this will likely need to be adjusted.
+ Glow = new CircularProgress
+ {
+ RelativeSizeAxes = Axes.Both,
+ Current = { Value = 1f / light_count - 0.01f },
+ Blending = BlendingParameters.Additive
+ }.WithEffect(new GlowEffect
+ {
+ Colour = colourProvider.Colour1.Opacity(0.4f),
+ BlurSigma = new Vector2(9f),
+ Strength = 10,
+ PadExtent = true
+ }),
+ }
+ },
+ };
+ }
+
+ public override void Show()
+ {
+ fillContent
+ .FadeIn(50, Easing.OutQuint)
+ .FlashColour(Color4.White, 1000, Easing.OutQuint);
+ }
+
+ public override void Hide()
+ {
+ fillContent
+ .FadeOut(300, Easing.OutQuint);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs
index d0ab4d1f98..9b5574d3cb 100644
--- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs
+++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs
@@ -1,29 +1,45 @@
// 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.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.Graphics.Sprites;
+using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
+using osuTK;
namespace osu.Game.Screens.Edit.Timing
{
public class TapTimingControl : CompositeDrawable
{
[Resolved]
- private EditorClock editorClock { get; set; }
+ private EditorClock editorClock { get; set; } = null!;
[Resolved]
- private Bindable selectedGroup { get; set; }
+ private EditorBeatmap beatmap { get; set; } = null!;
+
+ [Resolved]
+ private Bindable selectedGroup { get; set; } = null!;
+
+ private readonly BindableBool isHandlingTapping = new BindableBool();
+
+ private MetronomeDisplay metronome = null!;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuColour colours)
{
+ const float padding = 10;
+
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@@ -44,7 +60,8 @@ namespace osu.Game.Screens.Edit.Timing
RowDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 200),
- new Dimension(GridSizeMode.Absolute, 60),
+ new Dimension(GridSizeMode.Absolute, 50),
+ new Dimension(GridSizeMode.Absolute, TapButton.SIZE + padding),
},
Content = new[]
{
@@ -53,6 +70,7 @@ namespace osu.Game.Screens.Edit.Timing
new Container
{
RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding(padding),
Children = new Drawable[]
{
new GridContainer
@@ -67,7 +85,7 @@ namespace osu.Game.Screens.Edit.Timing
{
new Drawable[]
{
- new MetronomeDisplay
+ metronome = new MetronomeDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@@ -77,43 +95,95 @@ namespace osu.Game.Screens.Edit.Timing
},
}
}
- }
+ },
},
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding(10),
+ Padding = new MarginPadding { Bottom = padding, Horizontal = padding },
Children = new Drawable[]
{
- new RoundedButton
+ new TimingAdjustButton(1)
{
- Text = "Reset",
- BackgroundColour = colours.Pink,
- RelativeSizeAxes = Axes.X,
- Width = 0.3f,
- Action = reset,
+ Text = "Offset",
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.48f, 1),
+ Action = adjustOffset,
},
- new RoundedButton
+ new TimingAdjustButton(0.1)
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
- Text = "Play from start",
- RelativeSizeAxes = Axes.X,
- BackgroundColour = colourProvider.Background1,
- Width = 0.68f,
- Action = tap,
+ Text = "BPM",
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.48f, 1),
+ Action = adjustBpm,
}
}
},
- }
+ },
+ new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Bottom = padding, Horizontal = padding },
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.Y,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.CentreRight,
+ Height = 0.98f,
+ Width = TapButton.SIZE / 1.3f,
+ Masking = true,
+ CornerRadius = 15,
+ Children = new Drawable[]
+ {
+ new InlineButton(FontAwesome.Solid.Stop, Anchor.TopLeft)
+ {
+ BackgroundColour = colourProvider.Background1,
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.49f,
+ Action = reset,
+ },
+ new InlineButton(FontAwesome.Solid.Play, Anchor.BottomLeft)
+ {
+ BackgroundColour = colourProvider.Background1,
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.49f,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Action = start,
+ },
+ },
+ },
+ new TapButton
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ IsHandlingTapping = { BindTarget = isHandlingTapping }
+ }
+ }
+ },
+ },
}
},
};
+
+ isHandlingTapping.BindValueChanged(handling =>
+ {
+ metronome.EnableClicking = !handling.NewValue;
+
+ if (handling.NewValue)
+ start();
+ }, true);
}
- private void tap()
+ private void start()
{
editorClock.Seek(selectedGroup.Value.Time);
editorClock.Start();
@@ -124,5 +194,96 @@ namespace osu.Game.Screens.Edit.Timing
editorClock.Stop();
editorClock.Seek(selectedGroup.Value.Time);
}
+
+ private void adjustOffset(double adjust)
+ {
+ // VERY TEMPORARY
+ var currentGroupItems = selectedGroup.Value.ControlPoints.ToArray();
+
+ beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value);
+
+ double newOffset = selectedGroup.Value.Time + adjust;
+
+ foreach (var cp in currentGroupItems)
+ beatmap.ControlPointInfo.Add(newOffset, cp);
+
+ // the control point might not necessarily exist yet, if currentGroupItems was empty.
+ selectedGroup.Value = beatmap.ControlPointInfo.GroupAt(newOffset, true);
+
+ if (!editorClock.IsRunning)
+ editorClock.Seek(newOffset);
+ }
+
+ private void adjustBpm(double adjust)
+ {
+ var timing = selectedGroup.Value.ControlPoints.OfType().FirstOrDefault();
+
+ if (timing == null)
+ return;
+
+ timing.BeatLength = 60000 / (timing.BPM + adjust);
+ }
+
+ private class InlineButton : OsuButton
+ {
+ private readonly IconUsage icon;
+ private readonly Anchor anchor;
+
+ private SpriteIcon spriteIcon = null!;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ public InlineButton(IconUsage icon, Anchor anchor)
+ {
+ this.icon = icon;
+ this.anchor = anchor;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Content.CornerRadius = 0;
+ Content.Masking = false;
+
+ BackgroundColour = colourProvider.Background2;
+
+ Content.Add(new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding(15),
+ Children = new Drawable[]
+ {
+ spriteIcon = new SpriteIcon
+ {
+ Icon = icon,
+ Size = new Vector2(22),
+ Anchor = anchor,
+ Origin = anchor,
+ Colour = colourProvider.Background1,
+ },
+ }
+ });
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ // scale looks bad so don't call base.
+ return false;
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ spriteIcon.FadeColour(colourProvider.Content2, 200, Easing.OutQuint);
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ spriteIcon.FadeColour(colourProvider.Background1, 200, Easing.OutQuint);
+ base.OnHoverLost(e);
+ }
+ }
}
}
diff --git a/osu.Game/Screens/Edit/Timing/TimingAdjustButton.cs b/osu.Game/Screens/Edit/Timing/TimingAdjustButton.cs
new file mode 100644
index 0000000000..9540547d89
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/TimingAdjustButton.cs
@@ -0,0 +1,208 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Overlays;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ ///
+ /// A button with variable constant output based on hold position and length.
+ ///
+ public class TimingAdjustButton : CompositeDrawable
+ {
+ public Action Action;
+
+ private readonly double adjustAmount;
+
+ private const int max_multiplier = 10;
+ private const int adjust_levels = 4;
+
+ public Container Content { get; set; }
+
+ private readonly Box background;
+
+ private readonly OsuSpriteText text;
+
+ public LocalisableString Text
+ {
+ get => text.Text;
+ set => text.Text = value;
+ }
+
+ private readonly RepeatingButtonBehaviour repeatBehaviour;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; }
+
+ public TimingAdjustButton(double adjustAmount)
+ {
+ this.adjustAmount = adjustAmount;
+
+ CornerRadius = 5;
+ Masking = true;
+
+ AddInternal(Content = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ background = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Depth = float.MaxValue
+ },
+ text = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Font = OsuFont.Default.With(weight: FontWeight.SemiBold),
+ Padding = new MarginPadding(5),
+ Depth = float.MinValue
+ }
+ }
+ });
+
+ AddInternal(repeatBehaviour = new RepeatingButtonBehaviour(this));
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ background.Colour = colourProvider.Background3;
+
+ for (int i = 1; i <= adjust_levels; i++)
+ {
+ Content.Add(new IncrementBox(i, adjustAmount));
+ Content.Add(new IncrementBox(-i, adjustAmount));
+ }
+ }
+
+ protected override bool OnHover(HoverEvent e) => true;
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ var hoveredBox = Content.OfType().FirstOrDefault(d => d.IsHovered);
+ if (hoveredBox == null)
+ return false;
+
+ Action(adjustAmount * hoveredBox.Multiplier);
+
+ hoveredBox.Flash();
+
+ repeatBehaviour.SampleFrequencyModifier = (hoveredBox.Multiplier / max_multiplier) * 0.2;
+ return true;
+ }
+
+ private class IncrementBox : CompositeDrawable
+ {
+ public readonly float Multiplier;
+
+ private readonly Box box;
+ private readonly OsuSpriteText text;
+
+ public IncrementBox(int index, double amount)
+ {
+ Multiplier = Math.Sign(index) * convertMultiplier(index);
+
+ float ratio = (float)index / adjust_levels;
+
+ RelativeSizeAxes = Axes.Both;
+
+ Width = 0.5f * Math.Abs(ratio);
+
+ Anchor direction = index < 0 ? Anchor.x2 : Anchor.x0;
+
+ Origin |= direction;
+
+ Depth = Math.Abs(index);
+
+ Anchor = Anchor.TopCentre;
+
+ InternalChildren = new Drawable[]
+ {
+ box = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive
+ },
+ text = new OsuSpriteText
+ {
+ Anchor = direction,
+ Origin = direction,
+ Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold),
+ Text = $"{(index > 0 ? "+" : "-")}{Math.Abs(Multiplier * amount)}",
+ Padding = new MarginPadding(2),
+ Alpha = 0,
+ }
+ };
+ }
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ box.Colour = colourProvider.Background1;
+ box.Alpha = 0.1f;
+ }
+
+ private float convertMultiplier(int m)
+ {
+ switch (Math.Abs(m))
+ {
+ default: return 1;
+
+ case 2: return 2;
+
+ case 3: return 5;
+
+ case 4:
+ return max_multiplier;
+ }
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ box.Colour = colourProvider.Colour0;
+
+ box.FadeTo(0.2f, 100, Easing.OutQuint);
+ text.FadeIn(100, Easing.OutQuint);
+ return true;
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ box.Colour = colourProvider.Background1;
+
+ box.FadeTo(0.1f, 500, Easing.OutQuint);
+ text.FadeOut(100, Easing.OutQuint);
+ base.OnHoverLost(e);
+ }
+
+ public void Flash()
+ {
+ box
+ .FadeTo(0.4f, 20, Easing.OutQuint)
+ .Then()
+ .FadeTo(0.2f, 400, Easing.OutQuint);
+
+ text
+ .MoveToY(-5, 20, Easing.OutQuint)
+ .Then()
+ .MoveToY(0, 400, Easing.OutQuint);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs
index f71a8d7d22..f498aa917e 100644
--- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs
+++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs
@@ -1,12 +1,14 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
@@ -19,7 +21,7 @@ namespace osu.Game.Screens.Edit.Timing
public class TimingScreen : EditorScreenWithTimeline
{
[Cached]
- private Bindable selectedGroup = new Bindable();
+ public readonly Bindable SelectedGroup = new Bindable();
public TimingScreen()
: base(EditorScreenMode.Timing)
@@ -51,6 +53,8 @@ namespace osu.Game.Screens.Edit.Timing
private readonly IBindableList controlPointGroups = new BindableList();
+ private RoundedButton addButton;
+
[Resolved]
private EditorClock clock { get; set; }
@@ -105,9 +109,8 @@ namespace osu.Game.Screens.Edit.Timing
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
- new RoundedButton
+ addButton = new RoundedButton
{
- Text = "+ Add at current time",
Action = addNew,
Size = new Vector2(160, 30),
Anchor = Anchor.BottomRight,
@@ -122,7 +125,14 @@ namespace osu.Game.Screens.Edit.Timing
{
base.LoadComplete();
- selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true);
+ selectedGroup.BindValueChanged(selected =>
+ {
+ deleteButton.Enabled.Value = selected.NewValue != null;
+
+ addButton.Text = selected.NewValue != null
+ ? "+ Clone to current time"
+ : "+ Add at current time";
+ }, true);
controlPointGroups.BindTo(Beatmap.ControlPointInfo.Groups);
controlPointGroups.BindCollectionChanged((sender, args) =>
@@ -132,6 +142,61 @@ namespace osu.Game.Screens.Edit.Timing
}, true);
}
+ protected override bool OnClick(ClickEvent e)
+ {
+ selectedGroup.Value = null;
+ return true;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ trackActivePoint();
+
+ addButton.Enabled.Value = clock.CurrentTimeAccurate != selectedGroup.Value?.Time;
+ }
+
+ private Type trackedType;
+
+ ///
+ /// Given the user has selected a control point group, we want to track any group which is
+ /// active at the current point in time which matches the type the user has selected.
+ ///
+ /// So if the user is currently looking at a timing point and seeks into the future, a
+ /// future timing point would be automatically selected if it is now the new "current" point.
+ ///
+ private void trackActivePoint()
+ {
+ // For simplicity only match on the first type of the active control point.
+ if (selectedGroup.Value == null)
+ trackedType = null;
+ else
+ {
+ // If the selected group only has one control point, update the tracking type.
+ if (selectedGroup.Value.ControlPoints.Count == 1)
+ trackedType = selectedGroup.Value?.ControlPoints.Single().GetType();
+ // If the selected group has more than one control point, choose the first as the tracking type
+ // if we don't already have a singular tracked type.
+ else if (trackedType == null)
+ trackedType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType();
+ }
+
+ if (trackedType != null)
+ {
+ // We don't have an efficient way of looking up groups currently, only individual point types.
+ // To improve the efficiency of this in the future, we should reconsider the overall structure of ControlPointInfo.
+
+ // Find the next group which has the same type as the selected one.
+ var found = Beatmap.ControlPointInfo.Groups
+ .Where(g => g.ControlPoints.Any(cp => cp.GetType() == trackedType))
+ .LastOrDefault(g => g.Time <= clock.CurrentTimeAccurate);
+
+ if (found != null)
+ selectedGroup.Value = found;
+ }
+ }
+
private void delete()
{
if (selectedGroup.Value == null)
@@ -144,7 +209,27 @@ namespace osu.Game.Screens.Edit.Timing
private void addNew()
{
- selectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true);
+ bool isFirstControlPoint = !Beatmap.ControlPointInfo.TimingPoints.Any();
+
+ var group = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true);
+
+ if (isFirstControlPoint)
+ group.Add(new TimingControlPoint());
+ else
+ {
+ // Try and create matching types from the currently selected control point.
+ var selected = selectedGroup.Value;
+
+ if (selected != null && selected != group)
+ {
+ foreach (var controlPoint in selected.ControlPoints)
+ {
+ group.Add(controlPoint.DeepClone());
+ }
+ }
+ }
+
+ selectedGroup.Value = group;
}
}
}
diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs
index a5abd96d72..1a97058d73 100644
--- a/osu.Game/Screens/Edit/Timing/TimingSection.cs
+++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs
@@ -28,15 +28,31 @@ namespace osu.Game.Screens.Edit.Timing
});
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ bpmTextEntry.Current.BindValueChanged(_ => saveChanges());
+ timeSignature.Current.BindValueChanged(_ => saveChanges());
+
+ void saveChanges()
+ {
+ if (!isRebinding) ChangeHandler?.SaveState();
+ }
+ }
+
+ private bool isRebinding;
+
protected override void OnControlPointChanged(ValueChangedEvent point)
{
if (point.NewValue != null)
{
- bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable;
- bpmTextEntry.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
+ isRebinding = true;
+ bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable;
timeSignature.Current = point.NewValue.TimeSignatureBindable;
- timeSignature.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
+
+ isRebinding = false;
}
}
diff --git a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs
index c80d3c4261..0745187e43 100644
--- a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs
+++ b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs
@@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
@@ -26,6 +27,8 @@ namespace osu.Game.Screens.Edit.Timing
{
private const int total_waveforms = 8;
+ private const float corner_radius = LabelledDrawable.CORNER_RADIUS;
+
private readonly BindableNumber beatLength = new BindableDouble();
[Resolved]
@@ -42,18 +45,22 @@ namespace osu.Game.Screens.Edit.Timing
private TimingControlPoint timingPoint = TimingControlPoint.DEFAULT;
- private int lastDisplayedBeatIndex;
+ private double displayedTime;
private double selectedGroupStartTime;
private double selectedGroupEndTime;
private readonly IBindableList controlPointGroups = new BindableList();
+ private readonly BindableBool displayLocked = new BindableBool();
+
+ private LockedOverlay lockedOverlay = null!;
+
public WaveformComparisonDisplay()
{
RelativeSizeAxes = Axes.Both;
- CornerRadius = LabelledDrawable.CORNER_RADIUS;
+ CornerRadius = corner_radius;
Masking = true;
}
@@ -63,7 +70,7 @@ namespace osu.Game.Screens.Edit.Timing
for (int i = 0; i < total_waveforms; i++)
{
- AddInternal(new WaveformRow
+ AddInternal(new WaveformRow(i == total_waveforms / 2)
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
@@ -81,72 +88,112 @@ namespace osu.Game.Screens.Edit.Timing
Width = 3,
});
+ AddInternal(lockedOverlay = new LockedOverlay());
+
selectedGroup.BindValueChanged(_ => updateTimingGroup(), true);
controlPointGroups.BindTo(editorBeatmap.ControlPointInfo.Groups);
controlPointGroups.BindCollectionChanged((_, __) => updateTimingGroup());
- beatLength.BindValueChanged(_ => showFrom(lastDisplayedBeatIndex), true);
+ beatLength.BindValueChanged(_ => regenerateDisplay(true), true);
+
+ displayLocked.BindValueChanged(locked =>
+ {
+ if (locked.NewValue)
+ lockedOverlay.Show();
+ else
+ lockedOverlay.Hide();
+ }, true);
}
private void updateTimingGroup()
{
beatLength.UnbindBindings();
- selectedGroupStartTime = 0;
- selectedGroupEndTime = beatmap.Value.Track.Length;
-
var tcp = selectedGroup.Value?.ControlPoints.OfType().FirstOrDefault();
if (tcp == null)
{
timingPoint = new TimingControlPoint();
+ // During movement of a control point's offset, this clause can be hit momentarily,
+ // as moving a control point is implemented by removing it and inserting it at the new time.
+ // We don't want to reset the `selectedGroupStartTime` here as we rely on having the
+ // last value to update the waveform display below.
+ selectedGroupEndTime = beatmap.Value.Track.Length;
return;
}
timingPoint = tcp;
beatLength.BindTo(timingPoint.BeatLengthBindable);
- selectedGroupStartTime = selectedGroup.Value?.Time ?? 0;
+ double? newStartTime = selectedGroup.Value?.Time;
+ double? offsetChange = newStartTime - selectedGroupStartTime;
var nextGroup = editorBeatmap.ControlPointInfo.TimingPoints
.SkipWhile(g => g != tcp)
.Skip(1)
.FirstOrDefault();
- if (nextGroup != null)
- selectedGroupEndTime = nextGroup.Time;
+ selectedGroupStartTime = newStartTime ?? 0;
+ selectedGroupEndTime = nextGroup?.Time ?? beatmap.Value.Track.Length;
+
+ if (newStartTime.HasValue && offsetChange.HasValue)
+ {
+ // The offset of the selected point may have changed.
+ // This handles the case the user has locked the view and expects the display to update with this change.
+ showFromTime(displayedTime + offsetChange.Value, true);
+ }
}
protected override bool OnHover(HoverEvent e) => true;
protected override bool OnMouseMove(MouseMoveEvent e)
{
- float trackLength = (float)beatmap.Value.Track.Length;
- int totalBeatsAvailable = (int)(trackLength / timingPoint.BeatLength);
+ if (!displayLocked.Value)
+ {
+ float trackLength = (float)beatmap.Value.Track.Length;
+ int totalBeatsAvailable = (int)(trackLength / timingPoint.BeatLength);
- Scheduler.AddOnce(showFrom, (int)(e.MousePosition.X / DrawWidth * totalBeatsAvailable));
+ Scheduler.AddOnce(showFromBeat, (int)(e.MousePosition.X / DrawWidth * totalBeatsAvailable));
+ }
return base.OnMouseMove(e);
}
+ protected override bool OnClick(ClickEvent e)
+ {
+ displayLocked.Toggle();
+ return true;
+ }
+
protected override void Update()
{
base.Update();
- if (!IsHovered)
+ if (!IsHovered && !displayLocked.Value)
{
int currentBeat = (int)Math.Floor((editorClock.CurrentTimeAccurate - selectedGroupStartTime) / timingPoint.BeatLength);
- showFrom(currentBeat);
+ showFromBeat(currentBeat);
}
}
- private void showFrom(int beatIndex)
+ private void showFromBeat(int beatIndex) =>
+ showFromTime(selectedGroupStartTime + beatIndex * timingPoint.BeatLength, false);
+
+ private void showFromTime(double time, bool animated)
{
- if (lastDisplayedBeatIndex == beatIndex)
+ if (displayedTime == time)
return;
+ displayedTime = time;
+ regenerateDisplay(animated);
+ }
+
+ private void regenerateDisplay(bool animated)
+ {
+ double index = (displayedTime - selectedGroupStartTime) / timingPoint.BeatLength;
+
// Chosen as a pretty usable number across all BPMs.
// Optimally we'd want this to scale with the BPM in question, but performing
// scaling of the display is both expensive in resampling, and decreases usability
@@ -156,38 +203,115 @@ namespace osu.Game.Screens.Edit.Timing
float trackLength = (float)beatmap.Value.Track.Length;
float scale = trackLength / visible_width;
+ const int start_offset = total_waveforms / 2;
+
// Start displaying from before the current beat
- beatIndex -= total_waveforms / 2;
+ index -= start_offset;
foreach (var row in InternalChildren.OfType())
{
// offset to the required beat index.
- double time = selectedGroupStartTime + beatIndex * timingPoint.BeatLength;
+ double time = selectedGroupStartTime + index * timingPoint.BeatLength;
float offset = (float)(time - visible_width / 2) / trackLength * scale;
row.Alpha = time < selectedGroupStartTime || time > selectedGroupEndTime ? 0.2f : 1;
- row.WaveformOffset = -offset;
+ row.WaveformOffsetTo(-offset, animated);
row.WaveformScale = new Vector2(scale, 1);
- row.BeatIndex = beatIndex++;
+ row.BeatIndex = (int)Math.Floor(index);
+
+ index++;
+ }
+ }
+
+ internal class LockedOverlay : CompositeDrawable
+ {
+ private OsuSpriteText text = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ RelativeSizeAxes = Axes.Both;
+ Masking = true;
+ CornerRadius = corner_radius;
+ BorderColour = colours.Red;
+ BorderThickness = 3;
+ Alpha = 0;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ AlwaysPresent = true,
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ },
+ new Container
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Red,
+ RelativeSizeAxes = Axes.Both,
+ },
+ text = new OsuSpriteText
+ {
+ Colour = colours.GrayF,
+ Text = "Locked",
+ Margin = new MarginPadding(5),
+ Shadow = false,
+ Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold),
+ }
+ }
+ },
+ };
}
- lastDisplayedBeatIndex = beatIndex;
+ public override void Show()
+ {
+ this.FadeIn(100, Easing.OutQuint);
+
+ text
+ .FadeIn().Then().Delay(600)
+ .FadeOut().Then().Delay(600)
+ .Loop();
+ }
+
+ public override void Hide()
+ {
+ this.FadeOut(100, Easing.OutQuint);
+ }
}
internal class WaveformRow : CompositeDrawable
{
+ private readonly bool isMainRow;
private OsuSpriteText beatIndexText = null!;
private WaveformGraph waveformGraph = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
+ public WaveformRow(bool isMainRow)
+ {
+ this.isMainRow = isMainRow;
+ }
+
[BackgroundDependencyLoader]
private void load(IBindable beatmap)
{
InternalChildren = new Drawable[]
{
+ new Box
+ {
+ Colour = colourProvider.Background3,
+ Alpha = isMainRow ? 1 : 0,
+ RelativeSizeAxes = Axes.Both,
+ },
waveformGraph = new WaveformGraph
{
RelativeSizeAxes = Axes.Both,
@@ -212,7 +336,15 @@ namespace osu.Game.Screens.Edit.Timing
public int BeatIndex { set => beatIndexText.Text = value.ToString(); }
public Vector2 WaveformScale { set => waveformGraph.Scale = value; }
- public float WaveformOffset { set => waveformGraph.X = value; }
+
+ public void WaveformOffsetTo(float value, bool animated) =>
+ this.TransformTo(nameof(waveformOffset), value, animated ? 300 : 0, Easing.OutQuint);
+
+ private float waveformOffset
+ {
+ get => waveformGraph.X;
+ set => waveformGraph.X = value;
+ }
}
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs
index 35c903eb0c..0015cf8bf9 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs
@@ -55,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
private void load(AudioManager audio)
{
sampleSelect = audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select");
- sampleJoin = audio.Samples.Get($@"UI/{HoverSampleSet.Submit.GetDescription()}-select");
+ sampleJoin = audio.Samples.Get($@"UI/{HoverSampleSet.Button.GetDescription()}-select");
AddRangeInternal(new Drawable[]
{
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
index 3bf8a09cb9..43b128b971 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
@@ -82,7 +82,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true);
// todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area.
- LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(GameplayState.Ruleset.RulesetInfo, ScoreProcessor, users), l =>
+ LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(users), l =>
{
if (!LoadedBeatmapSuccessfully)
return;
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs
index 4545913db8..4e9ab07e4c 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs
@@ -2,19 +2,16 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using JetBrains.Annotations;
using osu.Framework.Timing;
using osu.Game.Online.Multiplayer;
-using osu.Game.Rulesets;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
public class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard
{
- public MultiSpectatorLeaderboard(RulesetInfo ruleset, [NotNull] ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users)
- : base(ruleset, scoreProcessor, users)
+ public MultiSpectatorLeaderboard(MultiplayerRoomUser[] users)
+ : base(users)
{
}
@@ -23,52 +20,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
if (!UserScores.TryGetValue(userId, out var data))
throw new ArgumentException(@"Provided user is not tracked by this leaderboard", nameof(userId));
- ((SpectatingTrackedUserData)data).Clock = clock;
+ data.ScoreProcessor.ReferenceClock = clock;
}
- public void RemoveClock(int userId)
- {
- if (!UserScores.TryGetValue(userId, out var data))
- throw new ArgumentException(@"Provided user is not tracked by this leaderboard", nameof(userId));
-
- ((SpectatingTrackedUserData)data).Clock = null;
- }
-
- protected override TrackedUserData CreateUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(user, ruleset, scoreProcessor);
-
protected override void Update()
{
base.Update();
foreach (var (_, data) in UserScores)
- data.UpdateScore();
- }
-
- private class SpectatingTrackedUserData : TrackedUserData
- {
- [CanBeNull]
- public IClock Clock;
-
- public SpectatingTrackedUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor)
- : base(user, ruleset, scoreProcessor)
- {
- }
-
- public override void UpdateScore()
- {
- if (Frames.Count == 0)
- return;
-
- if (Clock == null)
- return;
-
- int frameIndex = Frames.BinarySearch(new TimedFrame(Clock.CurrentTime));
- if (frameIndex < 0)
- frameIndex = ~frameIndex;
- frameIndex = Math.Clamp(frameIndex - 1, 0, Frames.Count - 1);
-
- SetFrame(Frames[frameIndex]);
- }
+ data.ScoreProcessor.UpdateScore();
}
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs
index 2d03276fe5..d9c19cdfdd 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs
@@ -128,12 +128,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
syncManager.AddPlayerClock(instances[i].GameplayClock);
}
- // Todo: This is not quite correct - it should be per-user to adjust for other mod combinations.
- var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
- var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor();
- scoreProcessor.ApplyBeatmap(playableBeatmap);
-
- LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(Ruleset.Value, scoreProcessor, users)
+ LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(users)
{
Expanded = { Value = true },
}, l =>
@@ -240,7 +235,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
instance.FadeColour(colours.Gray4, 400, Easing.OutQuint);
syncManager.RemovePlayerClock(instance.GameplayClock);
- leaderboard.RemoveClock(userId);
}
public override bool OnBackButton()
diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs
index c6a072da74..586cdcda51 100644
--- a/osu.Game/Screens/Play/GameplayState.cs
+++ b/osu.Game/Screens/Play/GameplayState.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 System;
using System.Collections.Generic;
using osu.Framework.Bindables;
@@ -8,10 +10,9 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
-#nullable enable
-
namespace osu.Game.Screens.Play
{
///
@@ -39,6 +40,8 @@ namespace osu.Game.Screens.Play
///
public readonly Score Score;
+ public readonly ScoreProcessor ScoreProcessor;
+
///
/// Whether gameplay completed without the user failing.
///
@@ -61,7 +64,7 @@ namespace osu.Game.Screens.Play
private readonly Bindable lastJudgementResult = new Bindable();
- public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList? mods = null, Score? score = null)
+ public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList? mods = null, Score? score = null, ScoreProcessor? scoreProcessor = null)
{
Beatmap = beatmap;
Ruleset = ruleset;
@@ -72,7 +75,8 @@ namespace osu.Game.Screens.Play
Ruleset = ruleset.RulesetInfo
}
};
- Mods = mods ?? ArraySegment.Empty;
+ Mods = mods ?? Array.Empty();
+ ScoreProcessor = scoreProcessor ?? ruleset.CreateScoreProcessor();
}
///
diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
index 41b40e9a91..5ee6000cf0 100644
--- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
+++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
+using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
@@ -17,9 +18,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Spectator;
-using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Scoring;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
@@ -43,8 +42,6 @@ namespace osu.Game.Screens.Play.HUD
[Resolved]
private UserLookupCache userLookupCache { get; set; }
- private readonly RulesetInfo ruleset;
- private readonly ScoreProcessor scoreProcessor;
private readonly MultiplayerRoomUser[] playingUsers;
private Bindable scoringMode;
@@ -55,57 +52,56 @@ namespace osu.Game.Screens.Play.HUD
///
/// Construct a new leaderboard.
///
- /// The ruleset.
- /// A score processor instance to handle score calculation for scores of users in the match.
/// IDs of all users in this match.
- public MultiplayerGameplayLeaderboard(RulesetInfo ruleset, ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users)
+ public MultiplayerGameplayLeaderboard(MultiplayerRoomUser[] users)
{
- // todo: this will eventually need to be created per user to support different mod combinations.
- this.ruleset = ruleset;
- this.scoreProcessor = scoreProcessor;
-
playingUsers = users;
}
[BackgroundDependencyLoader]
- private void load(OsuConfigManager config, IAPIProvider api)
+ private void load(OsuConfigManager config, IAPIProvider api, CancellationToken cancellationToken)
{
scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode);
foreach (var user in playingUsers)
{
- var trackedUser = CreateUserData(user, ruleset, scoreProcessor);
-
- trackedUser.ScoringMode.BindTo(scoringMode);
- trackedUser.Score.BindValueChanged(_ => Scheduler.AddOnce(updateTotals));
+ var scoreProcessor = new SpectatorScoreProcessor(user.UserID);
+ scoreProcessor.Mode.BindTo(scoringMode);
+ scoreProcessor.TotalScore.BindValueChanged(_ => Scheduler.AddOnce(updateTotals));
+ AddInternal(scoreProcessor);
+ var trackedUser = new TrackedUserData(user, scoreProcessor);
UserScores[user.UserID] = trackedUser;
if (trackedUser.Team is int team && !TeamScores.ContainsKey(team))
TeamScores.Add(team, new BindableLong());
}
- userLookupCache.GetUsersAsync(playingUsers.Select(u => u.UserID).ToArray()).ContinueWith(task => Schedule(() =>
- {
- var users = task.GetResultSafely();
+ userLookupCache.GetUsersAsync(playingUsers.Select(u => u.UserID).ToArray(), cancellationToken)
+ .ContinueWith(task =>
+ {
+ Schedule(() =>
+ {
+ var users = task.GetResultSafely();
- for (int i = 0; i < users.Length; i++)
- {
- var user = users[i] ?? new APIUser
- {
- Id = playingUsers[i].UserID,
- Username = "Unknown user",
- };
+ for (int i = 0; i < users.Length; i++)
+ {
+ var user = users[i] ?? new APIUser
+ {
+ Id = playingUsers[i].UserID,
+ Username = "Unknown user",
+ };
- var trackedUser = UserScores[user.Id];
+ var trackedUser = UserScores[user.Id];
- var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id);
- leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy);
- leaderboardScore.TotalScore.BindTo(trackedUser.Score);
- leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo);
- leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit);
- }
- }));
+ var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id);
+ leaderboardScore.Accuracy.BindTo(trackedUser.ScoreProcessor.Accuracy);
+ leaderboardScore.TotalScore.BindTo(trackedUser.ScoreProcessor.TotalScore);
+ leaderboardScore.Combo.BindTo(trackedUser.ScoreProcessor.Combo);
+ leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit);
+ }
+ });
+ }, cancellationToken);
}
protected override void LoadComplete()
@@ -118,20 +114,15 @@ namespace osu.Game.Screens.Play.HUD
spectatorClient.WatchUser(user.UserID);
if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(user.UserID))
- usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { user.UserID }));
+ playingUsersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { user.UserID }));
}
// bind here is to support players leaving the match.
// new players are not supported.
playingUserIds.BindTo(multiplayerClient.CurrentMatchPlayingUserIds);
- playingUserIds.BindCollectionChanged(usersChanged);
-
- // this leaderboard should be guaranteed to be completely loaded before the gameplay starts (is a prerequisite in MultiplayerPlayer).
- spectatorClient.OnNewFrames += handleIncomingFrames;
+ playingUserIds.BindCollectionChanged(playingUsersChanged);
}
- protected virtual TrackedUserData CreateUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor) => new TrackedUserData(user, ruleset, scoreProcessor);
-
protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(APIUser user, bool isTracked)
{
var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked);
@@ -157,7 +148,7 @@ namespace osu.Game.Screens.Play.HUD
}
}
- private void usersChanged(object sender, NotifyCollectionChangedEventArgs e)
+ private void playingUsersChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
@@ -174,15 +165,6 @@ namespace osu.Game.Screens.Play.HUD
}
}
- private void handleIncomingFrames(int userId, FrameDataBundle bundle) => Schedule(() =>
- {
- if (!UserScores.TryGetValue(userId, out var trackedData))
- return;
-
- trackedData.Frames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header));
- trackedData.UpdateScore();
- });
-
private void updateTotals()
{
if (!hasTeams)
@@ -196,7 +178,7 @@ namespace osu.Game.Screens.Play.HUD
continue;
if (TeamScores.TryGetValue(u.Team.Value, out var team))
- team.Value += (int)Math.Round(u.Score.Value);
+ team.Value += (int)Math.Round(u.ScoreProcessor.TotalScore.Value);
}
}
@@ -207,83 +189,26 @@ namespace osu.Game.Screens.Play.HUD
if (spectatorClient != null)
{
foreach (var user in playingUsers)
- {
spectatorClient.StopWatchingUser(user.UserID);
- }
-
- spectatorClient.OnNewFrames -= handleIncomingFrames;
}
}
protected class TrackedUserData
{
public readonly MultiplayerRoomUser User;
- public readonly ScoreProcessor ScoreProcessor;
+ public readonly SpectatorScoreProcessor ScoreProcessor;
- public readonly BindableDouble Score = new BindableDouble();
- public readonly BindableDouble Accuracy = new BindableDouble(1);
- public readonly BindableInt CurrentCombo = new BindableInt();
public readonly BindableBool UserQuit = new BindableBool();
- public readonly IBindable ScoringMode = new Bindable();
-
- public readonly List Frames = new List();
-
public int? Team => (User.MatchState as TeamVersusUserState)?.TeamID;
- private readonly ScoreInfo scoreInfo;
-
- public TrackedUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor)
+ public TrackedUserData(MultiplayerRoomUser user, SpectatorScoreProcessor scoreProcessor)
{
User = user;
ScoreProcessor = scoreProcessor;
-
- scoreInfo = new ScoreInfo { Ruleset = ruleset };
-
- ScoringMode.BindValueChanged(_ => UpdateScore());
}
public void MarkUserQuit() => UserQuit.Value = true;
-
- public virtual void UpdateScore()
- {
- if (Frames.Count == 0)
- return;
-
- SetFrame(Frames.Last());
- }
-
- protected void SetFrame(TimedFrame frame)
- {
- var header = frame.Header;
-
- scoreInfo.MaxCombo = header.MaxCombo;
- scoreInfo.Statistics = header.Statistics;
-
- Score.Value = ScoreProcessor.ComputePartialScore(ScoringMode.Value, scoreInfo);
-
- Accuracy.Value = header.Accuracy;
- CurrentCombo.Value = header.Combo;
- }
- }
-
- protected class TimedFrame : IComparable
- {
- public readonly double Time;
- public readonly FrameHeader Header;
-
- public TimedFrame(double time)
- {
- Time = time;
- }
-
- public TimedFrame(double time, FrameHeader header)
- {
- Time = time;
- Header = header;
- }
-
- public int CompareTo(TimedFrame other) => Time.CompareTo(other.Time);
}
}
}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 51c1e6b43b..dfc0fa1d1d 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -213,8 +213,8 @@ namespace osu.Game.Screens.Play
dependencies.CacheAs(DrawableRuleset);
ScoreProcessor = ruleset.CreateScoreProcessor();
- ScoreProcessor.ApplyBeatmap(playableBeatmap);
ScoreProcessor.Mods.Value = gameplayMods;
+ ScoreProcessor.ApplyBeatmap(playableBeatmap);
dependencies.CacheAs(ScoreProcessor);
@@ -237,7 +237,7 @@ namespace osu.Game.Screens.Play
Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
Score.ScoreInfo.Mods = gameplayMods;
- dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score));
+ dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor));
var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin);
diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
index 5b3129dad6..b924fbd5df 100644
--- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
@@ -159,6 +159,8 @@ namespace osu.Game.Screens.Ranking.Expanded
Origin = Anchor.TopCentre,
Text = beatmap.DifficultyName,
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold),
+ MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
+ Truncate = true,
},
new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12))
{
diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs
index a59f14647d..e62b285966 100644
--- a/osu.Game/Screens/Select/BeatmapCarousel.cs
+++ b/osu.Game/Screens/Select/BeatmapCarousel.cs
@@ -10,6 +10,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Caching;
+using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
@@ -97,6 +98,8 @@ namespace osu.Game.Screens.Select
protected readonly CarouselScrollContainer Scroll;
+ private readonly NoResultsPlaceholder noResultsPlaceholder;
+
private IEnumerable beatmapSets => root.Children.OfType();
// todo: only used for testing, maybe remove.
@@ -170,7 +173,8 @@ namespace osu.Game.Screens.Select
Scroll = new CarouselScrollContainer
{
RelativeSizeAxes = Axes.Both,
- }
+ },
+ noResultsPlaceholder = new NoResultsPlaceholder()
}
};
}
@@ -633,7 +637,7 @@ namespace osu.Game.Screens.Select
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
{
// handles the vertical size of the carousel changing (ie. on window resize when aspect ratio has changed).
- if ((invalidation & Invalidation.Layout) > 0)
+ if (invalidation.HasFlagFast(Invalidation.DrawSize))
itemsCache.Invalidate();
return base.OnInvalidate(invalidation, source);
@@ -648,8 +652,18 @@ namespace osu.Game.Screens.Select
// First we iterate over all non-filtered carousel items and populate their
// vertical position data.
if (revalidateItems)
+ {
updateYPositions();
+ if (visibleItems.Count == 0)
+ {
+ noResultsPlaceholder.Filter = activeCriteria;
+ noResultsPlaceholder.Show();
+ }
+ else
+ noResultsPlaceholder.Hide();
+ }
+
// if there is a pending scroll action we apply it without animation and transfer the difference in position to the panels.
// this is intentionally applied before updating the visible range below, to avoid animating new items (sourced from pool) from locations off-screen, as it looks bad.
if (pendingScrollOperation != PendingScrollOperation.None)
diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
index 9772b1feb3..98b885eb43 100644
--- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
+++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
@@ -8,6 +8,7 @@ using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -244,7 +245,7 @@ namespace osu.Game.Screens.Select.Carousel
}
if (hideRequested != null)
- items.Add(new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested(beatmapInfo)));
+ items.Add(new OsuMenuItem(CommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => hideRequested(beatmapInfo)));
return items.ToArray();
}
diff --git a/osu.Game/Screens/Select/Filter/SortMode.cs b/osu.Game/Screens/Select/Filter/SortMode.cs
index 5dfa2a2664..3dd3381059 100644
--- a/osu.Game/Screens/Select/Filter/SortMode.cs
+++ b/osu.Game/Screens/Select/Filter/SortMode.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Screens.Select.Filter
[Description("Author")]
Author,
- [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatsBpm))]
+ [LocalisableDescription(typeof(SortStrings), nameof(SortStrings.ArtistTracksBpm))]
BPM,
[Description("Date Added")]
@@ -28,10 +28,10 @@ namespace osu.Game.Screens.Select.Filter
Length,
// todo: pending support (https://github.com/ppy/osu/issues/4917)
- // [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchFiltersRank))]
+ // [Description("Rank Achieved")]
// RankAchieved,
- [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoSource))]
+ [Description("Source")]
Source,
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchSortingTitle))]
diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs
index 9cb178ca8b..1587f43258 100644
--- a/osu.Game/Screens/Select/FooterButton.cs
+++ b/osu.Game/Screens/Select/FooterButton.cs
@@ -66,7 +66,7 @@ namespace osu.Game.Screens.Select
private readonly Box light;
public FooterButton()
- : base(HoverSampleSet.Button)
+ : base(HoverSampleSet.Toolbar)
{
AutoSizeAxes = Axes.Both;
Shear = SHEAR;
diff --git a/osu.Game/Screens/Select/ImportFromStablePopup.cs b/osu.Game/Screens/Select/ImportFromStablePopup.cs
deleted file mode 100644
index d8137432bd..0000000000
--- a/osu.Game/Screens/Select/ImportFromStablePopup.cs
+++ /dev/null
@@ -1,33 +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.Graphics.Sprites;
-using osu.Game.Overlays.Dialog;
-
-namespace osu.Game.Screens.Select
-{
- public class ImportFromStablePopup : PopupDialog
- {
- public ImportFromStablePopup(Action importFromStable)
- {
- HeaderText = @"You have no beatmaps!";
- BodyText = "Would you like to import your beatmaps, skins, collections and scores from an existing osu!stable installation?\nThis will create a second copy of all files on disk.";
-
- Icon = FontAwesome.Solid.Plane;
-
- Buttons = new PopupDialogButton[]
- {
- new PopupDialogOkButton
- {
- Text = @"Yes please!",
- Action = importFromStable
- },
- new PopupDialogCancelButton
- {
- Text = @"No, I'd like to start from scratch",
- },
- };
- }
- }
-}
diff --git a/osu.Game/Screens/Select/NoResultsPlaceholder.cs b/osu.Game/Screens/Select/NoResultsPlaceholder.cs
new file mode 100644
index 0000000000..28a0541a22
--- /dev/null
+++ b/osu.Game/Screens/Select/NoResultsPlaceholder.cs
@@ -0,0 +1,145 @@
+// 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.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Localisation;
+using osu.Game.Online.Chat;
+using osu.Game.Overlays;
+using osuTK;
+
+namespace osu.Game.Screens.Select
+{
+ public class NoResultsPlaceholder : VisibilityContainer
+ {
+ private FilterCriteria? filter;
+
+ private LinkFlowContainer textFlow = null!;
+
+ [Resolved]
+ private BeatmapManager beatmaps { get; set; } = null!;
+
+ [Resolved]
+ private FirstRunSetupOverlay? firstRunSetupOverlay { get; set; }
+
+ [Resolved]
+ private OsuConfigManager config { get; set; } = null!;
+
+ public FilterCriteria Filter
+ {
+ set
+ {
+ if (filter == value)
+ return;
+
+ filter = value;
+ Scheduler.AddOnce(updateText);
+ }
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Masking = true;
+ CornerRadius = 10;
+
+ Width = 300;
+ AutoSizeAxes = Axes.Y;
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Gray2,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new SpriteIcon
+ {
+ Icon = FontAwesome.Regular.SadTear,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Margin = new MarginPadding(10),
+ Size = new Vector2(50),
+ },
+ textFlow = new LinkFlowContainer
+ {
+ Y = 60,
+ Padding = new MarginPadding(10),
+ TextAnchor = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ }
+ };
+ }
+
+ protected override void PopIn()
+ {
+ this.FadeIn(600, Easing.OutQuint);
+
+ Scheduler.AddOnce(updateText);
+ }
+
+ protected override void PopOut()
+ {
+ this.FadeOut(200, Easing.OutQuint);
+ }
+
+ private void updateText()
+ {
+ // TODO: Refresh this text when new beatmaps are imported. Right now it won't get up-to-date suggestions.
+
+ // Bounce should play every time the filter criteria is updated.
+ this.ScaleTo(0.9f)
+ .ScaleTo(1f, 1000, Easing.OutElastic);
+
+ textFlow.Clear();
+
+ if (beatmaps.QueryBeatmapSet(s => !s.Protected && !s.DeletePending) == null)
+ {
+ textFlow.AddParagraph("No beatmaps found!");
+ textFlow.AddParagraph(string.Empty);
+
+ textFlow.AddParagraph("Consider using the \"");
+ textFlow.AddLink(FirstRunSetupOverlayStrings.FirstRunSetupTitle, () => firstRunSetupOverlay?.Show());
+ textFlow.AddText("\" to download or import some beatmaps!");
+ }
+ else
+ {
+ textFlow.AddParagraph("No beatmaps match your filter criteria!");
+ textFlow.AddParagraph(string.Empty);
+
+ if (string.IsNullOrEmpty(filter?.SearchText))
+ {
+ // TODO: Add realm queries to hint at which ruleset results are available in (and allow clicking to switch).
+ // TODO: Make this message more certain by ensuring the osu! beatmaps exist before suggesting.
+ if (filter?.Ruleset.OnlineID > 0 && !filter.AllowConvertedBeatmaps)
+ {
+ textFlow.AddParagraph("Beatmaps may be available by ");
+ textFlow.AddLink("enabling automatic conversion", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
+ textFlow.AddText("!");
+ }
+ }
+ else
+ {
+ textFlow.AddParagraph("You can try ");
+ textFlow.AddLink("searching online", LinkAction.SearchBeatmapSet, filter.SearchText);
+ textFlow.AddText(" for this query.");
+ }
+ }
+
+ // TODO: add clickable link to reset criteria.
+ }
+ }
+}
diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs
index 6ecb96f723..df7dd47ef3 100644
--- a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs
+++ b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs
@@ -77,7 +77,7 @@ namespace osu.Game.Screens.Select.Options
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
public BeatmapOptionsButton()
- : base(HoverSampleSet.Submit)
+ : base(HoverSampleSet.Button)
{
Width = width;
RelativeSizeAxes = Axes.Y;
diff --git a/osu.Game/Screens/Select/SkinDeleteDialog.cs b/osu.Game/Screens/Select/SkinDeleteDialog.cs
new file mode 100644
index 0000000000..4262118658
--- /dev/null
+++ b/osu.Game/Screens/Select/SkinDeleteDialog.cs
@@ -0,0 +1,42 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Skinning;
+using osu.Game.Overlays.Dialog;
+
+namespace osu.Game.Screens.Select
+{
+ public class SkinDeleteDialog : PopupDialog
+ {
+ [Resolved]
+ private SkinManager manager { get; set; }
+
+ public SkinDeleteDialog(Skin skin)
+ {
+ BodyText = skin.SkinInfo.Value.Name;
+ Icon = FontAwesome.Regular.TrashAlt;
+ HeaderText = @"Confirm deletion of";
+ Buttons = new PopupDialogButton[]
+ {
+ new PopupDialogDangerousButton
+ {
+ Text = @"Yes. Totally. Delete it.",
+ Action = () =>
+ {
+ if (manager == null)
+ return;
+
+ manager.Delete(skin.SkinInfo.Value);
+ manager.CurrentSkinInfo.SetDefault();
+ },
+ },
+ new PopupDialogCancelButton
+ {
+ Text = @"Firetruck, I didn't mean to!",
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 8870239485..3bfdc845ab 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -28,7 +28,6 @@ using osuTK.Input;
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Threading.Tasks;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
@@ -37,7 +36,6 @@ using osu.Game.Graphics.UserInterface;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Game.Screens.Play;
-using osu.Game.Database;
using osu.Game.Skinning;
namespace osu.Game.Screens.Select
@@ -59,8 +57,6 @@ namespace osu.Game.Screens.Select
protected virtual bool ShowFooter => true;
- protected virtual bool DisplayStableImportPrompt => legacyImportManager?.SupportsImportFromStable == true;
-
public override bool? AllowTrackAdjustments => true;
///
@@ -94,14 +90,13 @@ namespace osu.Game.Screens.Select
protected Container LeftArea { get; private set; }
private BeatmapInfoWedge beatmapInfoWedge;
- private IDialogOverlay dialogOverlay;
+
+ [Resolved(canBeNull: true)]
+ private IDialogOverlay dialogOverlay { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
- [Resolved(CanBeNull = true)]
- private LegacyImportManager legacyImportManager { get; set; }
-
protected ModSelectOverlay ModSelect { get; private set; }
protected Sample SampleConfirm { get; private set; }
@@ -127,7 +122,7 @@ namespace osu.Game.Screens.Select
internal IOverlayManager OverlayManager { get; private set; }
[BackgroundDependencyLoader(true)]
- private void load(AudioManager audio, IDialogOverlay dialog, OsuColour colours, ManageCollectionsDialog manageCollectionsDialog, DifficultyRecommender recommender)
+ private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog manageCollectionsDialog, DifficultyRecommender recommender)
{
// initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter).
transferRulesetValue();
@@ -289,26 +284,9 @@ namespace osu.Game.Screens.Select
BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo));
}
- dialogOverlay = dialog;
-
sampleChangeDifficulty = audio.Samples.Get(@"SongSelect/select-difficulty");
sampleChangeBeatmap = audio.Samples.Get(@"SongSelect/select-expand");
SampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection");
-
- if (dialogOverlay != null)
- {
- Schedule(() =>
- {
- // if we have no beatmaps, let's prompt the user to import from over a stable install if he has one.
- if (beatmaps.QueryBeatmapSet(s => !s.Protected && !s.DeletePending) == null && DisplayStableImportPrompt)
- {
- dialogOverlay.Push(new ImportFromStablePopup(() =>
- {
- Task.Run(() => legacyImportManager.ImportFromStableAsync(StableContent.All));
- }));
- }
- });
- }
}
protected override void LoadComplete()
diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs
index e36d5ca3c6..095763de18 100644
--- a/osu.Game/Skinning/Editor/SkinEditor.cs
+++ b/osu.Game/Skinning/Editor/SkinEditor.cs
@@ -29,6 +29,8 @@ namespace osu.Game.Skinning.Editor
{
public const double TRANSITION_DURATION = 500;
+ public const float MENU_HEIGHT = 40;
+
public readonly BindableList SelectedComponents = new BindableList();
protected override bool StartHidden => true;
@@ -78,8 +80,6 @@ namespace osu.Game.Skinning.Editor
{
RelativeSizeAxes = Axes.Both;
- const float menu_height = 40;
-
InternalChild = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
@@ -102,7 +102,7 @@ namespace osu.Game.Skinning.Editor
Name = "Menu container",
RelativeSizeAxes = Axes.X,
Depth = float.MinValue,
- Height = menu_height,
+ Height = MENU_HEIGHT,
Children = new Drawable[]
{
new EditorMenuBar
@@ -322,7 +322,10 @@ namespace osu.Game.Skinning.Editor
protected override void PopIn()
{
- this.FadeIn(TRANSITION_DURATION, Easing.OutQuint);
+ this
+ // align animation to happen after the majority of the ScalingContainer animation completes.
+ .Delay(ScalingContainer.TRANSITION_DURATION * 0.3f)
+ .FadeIn(TRANSITION_DURATION, Easing.OutQuint);
}
protected override void PopOut()
diff --git a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs
index 497283a820..a3110ced24 100644
--- a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs
+++ b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs
@@ -12,6 +12,8 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.Screens;
+using osu.Game.Screens.Edit.Components;
+using osuTK;
namespace osu.Game.Skinning.Editor
{
@@ -33,6 +35,8 @@ namespace osu.Game.Skinning.Editor
private OsuScreen lastTargetScreen;
+ private Vector2 lastDrawSize;
+
public SkinEditorOverlay(ScalingContainer scalingContainer)
{
this.scalingContainer = scalingContainer;
@@ -81,15 +85,42 @@ namespace osu.Game.Skinning.Editor
protected override void PopOut() => skinEditor?.Hide();
+ protected override void Update()
+ {
+ base.Update();
+
+ if (game.DrawSize != lastDrawSize)
+ {
+ lastDrawSize = game.DrawSize;
+ updateScreenSizing();
+ }
+ }
+
+ private void updateScreenSizing()
+ {
+ if (skinEditor?.State.Value != Visibility.Visible) return;
+
+ const float padding = 10;
+
+ float relativeSidebarWidth = (EditorSidebar.WIDTH + padding) / DrawWidth;
+ float relativeToolbarHeight = (SkinEditorSceneLibrary.HEIGHT + SkinEditor.MENU_HEIGHT + padding) / DrawHeight;
+
+ var rect = new RectangleF(
+ relativeSidebarWidth,
+ relativeToolbarHeight,
+ 1 - relativeSidebarWidth * 2,
+ 1f - relativeToolbarHeight - padding / DrawHeight);
+
+ scalingContainer.SetCustomRect(rect, true);
+ }
+
private void updateComponentVisibility()
{
Debug.Assert(skinEditor != null);
- const float toolbar_padding_requirement = 0.18f;
-
if (skinEditor.State.Value == Visibility.Visible)
{
- scalingContainer.SetCustomRect(new RectangleF(toolbar_padding_requirement, 0.2f, 0.8f - toolbar_padding_requirement, 0.7f), true);
+ Scheduler.AddOnce(updateScreenSizing);
game?.Toolbar.Hide();
game?.CloseAllOverlays();
@@ -127,6 +158,9 @@ namespace osu.Game.Skinning.Editor
private void setTarget(OsuScreen target)
{
+ if (target == null)
+ return;
+
Debug.Assert(skinEditor != null);
if (!target.IsLoaded)
diff --git a/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs b/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs
index 2124ba9b6d..dc5a8aefc0 100644
--- a/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs
+++ b/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs
@@ -27,6 +27,8 @@ namespace osu.Game.Skinning.Editor
{
public class SkinEditorSceneLibrary : CompositeDrawable
{
+ public const float HEIGHT = BUTTON_HEIGHT + padding * 2;
+
public const float BUTTON_HEIGHT = 40;
private const float padding = 10;
@@ -42,7 +44,7 @@ namespace osu.Game.Skinning.Editor
public SkinEditorSceneLibrary()
{
- Height = BUTTON_HEIGHT + padding * 2;
+ Height = HEIGHT;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index b65ba8b04c..9524d3f615 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -303,8 +303,13 @@ namespace osu.Game.Skinning
if (Configuration.ConfigDictionary.TryGetValue(lookup.ToString(), out string val))
{
// special case for handling skins which use 1 or 0 to signify a boolean state.
+ // ..or in some cases 2 (https://github.com/ppy/osu/issues/18579).
if (typeof(TValue) == typeof(bool))
- val = val == "1" ? "true" : "false";
+ {
+ val = bool.TryParse(val, out bool boolVal)
+ ? Convert.ChangeType(boolVal, typeof(bool)).ToString()
+ : Convert.ChangeType(Convert.ToInt32(val), typeof(bool)).ToString();
+ }
var bindable = new Bindable();
if (val != null)
diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs
index 9481fc7182..34714b9dc5 100644
--- a/osu.Game/Skinning/LegacySkinTransformer.cs
+++ b/osu.Game/Skinning/LegacySkinTransformer.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Skinning
/// The which is being transformed.
///
[NotNull]
- protected internal ISkin Skin { get; }
+ public ISkin Skin { get; }
protected LegacySkinTransformer([NotNull] ISkin skin)
{
diff --git a/osu.Game/Tests/Gameplay/TestGameplayState.cs b/osu.Game/Tests/Gameplay/TestGameplayState.cs
index 0d00f52d15..f14f8c44ec 100644
--- a/osu.Game/Tests/Gameplay/TestGameplayState.cs
+++ b/osu.Game/Tests/Gameplay/TestGameplayState.cs
@@ -26,7 +26,10 @@ namespace osu.Game.Tests.Gameplay
var workingBeatmap = new TestWorkingBeatmap(beatmap);
var playableBeatmap = workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods);
- return new GameplayState(playableBeatmap, ruleset, mods, score);
+ var scoreProcessor = ruleset.CreateScoreProcessor();
+ scoreProcessor.ApplyBeatmap(playableBeatmap);
+
+ return new GameplayState(playableBeatmap, ruleset, mods, score, scoreProcessor);
}
}
}
diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs
index b6a347a896..df3974664e 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -67,7 +68,25 @@ namespace osu.Game.Tests.Visual.OnlinePlay
// To get around this, the BeatmapManager is looked up from the dependencies provided to the children of the test scene instead.
var beatmapManager = dependencies.Get();
- ((DummyAPIAccess)API).HandleRequest = request => handler.HandleRequest(request, API.LocalUser.Value, beatmapManager);
+ ((DummyAPIAccess)API).HandleRequest = request =>
+ {
+ TaskCompletionSource tcs = new TaskCompletionSource();
+
+ // Because some of the handlers use realm, we need to ensure the game is still alive when firing.
+ // If we don't, a stray `PerformAsync` could hit an `ObjectDisposedException` if running too late.
+ Scheduler.Add(() =>
+ {
+ bool result = handler.HandleRequest(request, API.LocalUser.Value, beatmapManager);
+ tcs.SetResult(result);
+ }, false);
+
+#pragma warning disable RS0030
+ // We can't GetResultSafely() here (will fail with "Can't use GetResultSafely from inside an async operation."), but Wait is safe enough due to
+ // the task being a TaskCompletionSource.
+ // Importantly, this doesn't deadlock because of the scheduler call above running inline where feasible (see the `false` argument).
+ return tcs.Task.Result;
+#pragma warning restore RS0030
+ };
});
///
diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
index 8290af8f78..8fea77833e 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
@@ -3,7 +3,9 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
+using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
@@ -44,9 +46,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
switch (request)
{
case CreateRoomRequest createRoomRequest:
- var apiRoom = new Room();
-
- apiRoom.CopyFrom(createRoomRequest.Room);
+ var apiRoom = cloneRoom(createRoomRequest.Room);
// Passwords are explicitly not copied between rooms.
apiRoom.HasPassword.Value = !string.IsNullOrEmpty(createRoomRequest.Room.Password.Value);
@@ -178,12 +178,31 @@ namespace osu.Game.Tests.Visual.OnlinePlay
private Room createResponseRoom(Room room, bool withParticipants)
{
- var responseRoom = new Room();
- responseRoom.CopyFrom(room);
+ var responseRoom = cloneRoom(room);
+
+ // Password is hidden from the response, and is only propagated via HasPassword.
+ bool hadPassword = responseRoom.HasPassword.Value;
responseRoom.Password.Value = null;
+ responseRoom.HasPassword.Value = hadPassword;
+
if (!withParticipants)
responseRoom.RecentParticipants.Clear();
+
return responseRoom;
}
+
+ private Room cloneRoom(Room source)
+ {
+ var result = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(source));
+ Debug.Assert(result != null);
+
+ // Playlist item IDs aren't serialised.
+ if (source.CurrentPlaylistItem.Value != null)
+ result.CurrentPlaylistItem.Value.ID = source.CurrentPlaylistItem.Value.ID;
+ for (int i = 0; i < source.Playlist.Count; i++)
+ result.Playlist[i].ID = source.Playlist[i].ID;
+
+ return result;
+ }
}
}
diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs
index 0dd135b500..d85648c078 100644
--- a/osu.Game/Users/Drawables/ClickableAvatar.cs
+++ b/osu.Game/Users/Drawables/ClickableAvatar.cs
@@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Users.Drawables
@@ -74,11 +73,6 @@ namespace osu.Game.Users.Drawables
{
private LocalisableString tooltip = default_tooltip_text;
- public ClickableArea()
- : base(HoverSampleSet.Submit)
- {
- }
-
public override LocalisableString TooltipText
{
get => Enabled.Value ? tooltip : default;
diff --git a/osu.Game/Users/Drawables/UpdateableFlag.cs b/osu.Game/Users/Drawables/UpdateableFlag.cs
index e5debc0683..d0ef760e59 100644
--- a/osu.Game/Users/Drawables/UpdateableFlag.cs
+++ b/osu.Game/Users/Drawables/UpdateableFlag.cs
@@ -49,7 +49,7 @@ namespace osu.Game.Users.Drawables
{
RelativeSizeAxes = Axes.Both
},
- new HoverClickSounds(HoverSampleSet.Submit)
+ new HoverClickSounds()
}
};
}
diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs
index 248debf1d3..40d70ca406 100644
--- a/osu.Game/Users/UserPanel.cs
+++ b/osu.Game/Users/UserPanel.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Users
protected Drawable Background { get; private set; }
protected UserPanel(APIUser user)
- : base(HoverSampleSet.Submit)
+ : base(HoverSampleSet.Button)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs
index 137bf7e0aa..c12fd607b4 100644
--- a/osu.Game/Utils/SentryLogger.cs
+++ b/osu.Game/Utils/SentryLogger.cs
@@ -43,14 +43,13 @@ namespace osu.Game.Utils
sentrySession = SentrySdk.Init(options =>
{
// Not setting the dsn will completely disable sentry.
- if (game.IsDeployedBuild)
+ if (game.IsDeployedBuild && game.CreateEndpoints().WebsiteRootUrl.EndsWith(@".ppy.sh", StringComparison.Ordinal))
options.Dsn = "https://ad9f78529cef40ac874afb95a9aca04e@sentry.ppy.sh/2";
options.AutoSessionTracking = true;
options.IsEnvironmentUser = false;
- // The reported release needs to match release tags on github in order for sentry
- // to automatically associate and track against releases.
- options.Release = game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty);
+ // The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml
+ options.Release = $"osu@{game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty)}";
});
Logger.NewEntry += processLogEntry;
@@ -160,6 +159,7 @@ namespace osu.Game.Utils
Game = game.Clock.CurrentTime,
};
+ scope.SetTag(@"beatmap", $"{beatmap.OnlineID}");
scope.SetTag(@"ruleset", ruleset.ShortName);
scope.SetTag(@"os", $"{RuntimeInfo.OS} ({Environment.OSVersion})");
scope.SetTag(@"processor count", Environment.ProcessorCount.ToString());
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 79cfd7c917..63b8cf4cb5 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -29,13 +29,14 @@
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index b1ba64beba..a0fafa635b 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,7 +61,7 @@
-
+
@@ -84,11 +84,11 @@
-
+
-
+
diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings
index 68cf8138e2..286a1eb29f 100644
--- a/osu.sln.DotSettings
+++ b/osu.sln.DotSettings
@@ -790,6 +790,15 @@ See the LICENCE file in the repository root for full licence text.
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
True
True
True