diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs
index 0d0c0d8f68..66269f5572 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Acronym => "CS";
- public override double ScoreMultiplier => 1;
+ public override double ScoreMultiplier => 0.9;
public override LocalisableString Description => "No more tricky speed changes!";
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index 46f7c461f8..7e995f2dde 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -17,18 +17,18 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
- [TestCase(6.6369583000323935d, 206, "diffcalc-test")]
- [TestCase(1.4476531024675374d, 45, "zero-length-sliders")]
+ [TestCase(6.7115569159190587d, 206, "diffcalc-test")]
+ [TestCase(1.4391311903612753d, 45, "zero-length-sliders")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(8.8816128335486386d, 206, "diffcalc-test")]
- [TestCase(1.7540389962596916d, 45, "zero-length-sliders")]
+ [TestCase(8.9757300665532966d, 206, "diffcalc-test")]
+ [TestCase(1.7437232654020756d, 45, "zero-length-sliders")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
- [TestCase(6.6369583000323935d, 239, "diffcalc-test")]
- [TestCase(1.4476531024675374d, 54, "zero-length-sliders")]
+ [TestCase(6.7115569159190587d, 239, "diffcalc-test")]
+ [TestCase(1.4391311903612753d, 54, "zero-length-sliders")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
index 76d5ccf682..6d1b4d1a15 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
@@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
public static class AimEvaluator
{
private const double wide_angle_multiplier = 1.5;
- private const double acute_angle_multiplier = 2.0;
- private const double slider_multiplier = 1.5;
+ private const double acute_angle_multiplier = 1.95;
+ private const double slider_multiplier = 1.35;
private const double velocity_change_multiplier = 0.75;
///
@@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
}
- if (osuLastObj.TravelTime != 0)
+ if (osuLastObj.BaseObject is Slider)
{
// Reward sliders based on velocity.
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs
index 3b0826394c..fcf4179a3b 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs
@@ -15,11 +15,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
private const double max_opacity_bonus = 0.4;
private const double hidden_bonus = 0.2;
+ private const double min_velocity = 0.5;
+ private const double slider_multiplier = 1.3;
+
///
/// Evaluates the difficulty of memorising and hitting an object, based on:
///
- /// - distance between the previous and current object,
+ /// - distance between a number of previous objects and the current object,
/// - the visual opacity of the current object,
+ /// - length and speed of the current object (for sliders),
/// - and whether the hidden mod is enabled.
///
///
@@ -73,6 +77,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
if (hidden)
result *= 1.0 + hidden_bonus;
+ double sliderBonus = 0.0;
+
+ if (osuCurrent.BaseObject is Slider osuSlider)
+ {
+ // Invert the scaling factor to determine the true travel distance independent of circle size.
+ double pixelTravelDistance = osuSlider.LazyTravelDistance / scalingFactor;
+
+ // Reward sliders based on velocity.
+ sliderBonus = Math.Pow(Math.Max(0.0, pixelTravelDistance / osuCurrent.TravelTime - min_velocity), 0.5);
+
+ // Longer sliders require more memorisation.
+ sliderBonus *= pixelTravelDistance;
+
+ // Nerf sliders with repeats, as less memorisation is required.
+ if (osuSlider.RepeatCount > 0)
+ sliderBonus /= (osuSlider.RepeatCount + 1);
+ }
+
+ result += sliderBonus * slider_multiplier;
+
return result;
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index 9f4a405113..0ebfb9a283 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -46,7 +46,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
if (mods.Any(h => h is OsuModRelax))
+ {
+ aimRating *= 0.9;
speedRating = 0.0;
+ flashlightRating *= 0.7;
+ }
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
@@ -62,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
);
- double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
+ double starRating = basePerformance > 0.00001 ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index c3b7834009..3c82c2dc33 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuPerformanceCalculator : PerformanceCalculator
{
+ public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
+
private double accuracy;
private int scoreMaxCombo;
private int countGreat;
@@ -41,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
- double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
+ double multiplier = PERFORMANCE_BASE_MULTIPLIER;
if (score.Mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
@@ -51,10 +53,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (score.Mods.Any(h => h is OsuModRelax))
{
- // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
- effectiveMissCount = Math.Min(effectiveMissCount + countOk + countMeh, totalHits);
+ // https://www.desmos.com/calculator/bc9eybdthb
+ // we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0
+ // this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11)
+ double okMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 1.8) : 1.0);
+ double mehMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 5) : 1.0);
- multiplier *= 0.6;
+ // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
+ effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
}
double aimValue = computeAimValue(score, osuAttributes);
@@ -103,7 +109,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (attributes.ApproachRate > 10.33)
approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
else if (attributes.ApproachRate < 8.0)
- approachRateFactor = 0.1 * (8.0 - attributes.ApproachRate);
+ approachRateFactor = 0.05 * (8.0 - attributes.ApproachRate);
+
+ if (score.Mods.Any(h => h is OsuModRelax))
+ approachRateFactor = 0.0;
aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
@@ -134,6 +143,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
+ if (score.Mods.Any(h => h is OsuModRelax))
+ return 0.0;
+
double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
@@ -174,7 +186,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping.
- speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
+ speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
return speedValue;
}
@@ -266,6 +278,5 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
private int totalHits => countGreat + countOk + countMeh + countMiss;
- private int totalSuccessfulHits => countGreat + countOk + countMeh;
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index c8646ec456..c7c5650184 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -15,10 +15,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{
public class OsuDifficultyHitObject : DifficultyHitObject
{
- private const int normalised_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
+ ///
+ /// A distance by which all distances should be scaled in order to assume a uniform circle size.
+ ///
+ public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
+
private const int min_delta_time = 25;
- private const float maximum_slider_radius = normalised_radius * 2.4f;
- private const float assumed_slider_radius = normalised_radius * 1.8f;
+ private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f;
+ private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f;
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
@@ -64,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
public double TravelDistance { get; private set; }
///
- /// The time taken to travel through , with a minimum value of 25ms for a non-zero distance.
+ /// The time taken to travel through , with a minimum value of 25ms for objects.
///
public double TravelTime { get; private set; }
@@ -123,7 +127,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (BaseObject is Slider currentSlider)
{
computeSliderCursorPosition(currentSlider);
- TravelDistance = currentSlider.LazyTravelDistance;
+ // Bonus for repeat sliders until a better per nested object strain system can be achieved.
+ TravelDistance = currentSlider.LazyTravelDistance * (float)Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5);
TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time);
}
@@ -132,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
return;
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
- float scalingFactor = normalised_radius / (float)BaseObject.Radius;
+ float scalingFactor = NORMALISED_RADIUS / (float)BaseObject.Radius;
if (BaseObject.Radius < 30)
{
@@ -206,7 +211,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived.
var currCursorPosition = slider.StackedPosition;
- double scalingFactor = normalised_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
+ double scalingFactor = NORMALISED_RADIUS / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
for (int i = 1; i < slider.NestedHitObjects.Count; i++)
{
@@ -234,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
else if (currMovementObj is SliderRepeat)
{
// For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.
- requiredMovement = normalised_radius;
+ requiredMovement = NORMALISED_RADIUS;
}
if (currMovementLength > requiredMovement)
@@ -248,8 +253,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (i == slider.NestedHitObjects.Count - 1)
slider.LazyEndPosition = currCursorPosition;
}
-
- slider.LazyTravelDistance *= (float)Math.Pow(1 + slider.RepeatCount / 2.5, 1.0 / 2.5); // Bonus for repeat sliders until a better per nested object strain system can be achieved.
}
private Vector2 getEndCursorPosition(OsuHitObject hitObject)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index 9b1fbf9a2e..38e0e5b677 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private double currentStrain;
- private double skillMultiplier => 23.25;
+ private double skillMultiplier => 23.55;
private double strainDecayBase => 0.15;
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
index a42eaec96b..f942538ed3 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => OsuIcon.ModAutopilot;
public override ModType Type => ModType.Automation;
public override LocalisableString Description => @"Automatic cursor movement - just follow the rhythm.";
- public override double ScoreMultiplier => 1;
+ public override double ScoreMultiplier => 0.1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) };
public bool PerformFail() => false;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
index 6871c26750..35a34094d1 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => FontAwesome.Solid.Magnet;
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "No need to chase the circles – your cursor is a magnet!";
- public override double ScoreMultiplier => 1;
+ public override double ScoreMultiplier => 0.5;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) };
private IFrameStableClock gameplayClock = null!;
diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs
index ee29cc8644..6bce03869d 100644
--- a/osu.Game.Tests/Resources/TestResources.cs
+++ b/osu.Game.Tests/Resources/TestResources.cs
@@ -128,6 +128,8 @@ namespace osu.Game.Tests.Resources
var rulesetInfo = getRuleset();
+ string hash = Guid.NewGuid().ToString().ComputeMD5Hash();
+
yield return new BeatmapInfo
{
OnlineID = beatmapId,
@@ -136,7 +138,8 @@ namespace osu.Game.Tests.Resources
Length = length,
BeatmapSet = beatmapSet,
BPM = bpm,
- Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
+ Hash = hash,
+ MD5Hash = hash,
Ruleset = rulesetInfo,
Metadata = metadata.DeepClone(),
Difficulty = new BeatmapDifficulty
diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs
index ee5ef2f364..e67eebf721 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs
@@ -1,18 +1,20 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
-using osu.Framework.Bindables;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Collections;
using osu.Game.Database;
-using osu.Game.Graphics.Containers;
using osu.Game.Overlays.Music;
+using osu.Game.Rulesets;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Input;
@@ -21,13 +23,25 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public class TestScenePlaylistOverlay : OsuManualInputManagerTestScene
{
- private readonly BindableList> beatmapSets = new BindableList>();
+ protected override bool UseFreshStoragePerRun => true;
- private PlaylistOverlay playlistOverlay;
+ private PlaylistOverlay playlistOverlay = null!;
- private Live first;
+ private BeatmapManager beatmapManager = null!;
- private const int item_count = 100;
+ private const int item_count = 20;
+
+ private List beatmapSets => beatmapManager.GetAllUsableBeatmapSets();
+
+ [BackgroundDependencyLoader]
+ private void load(GameHost host)
+ {
+ Dependencies.Cache(new RealmRulesetStore(Realm));
+ Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
+ Dependencies.Cache(Realm);
+
+ beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
+ }
[SetUp]
public void Setup() => Schedule(() =>
@@ -46,16 +60,12 @@ namespace osu.Game.Tests.Visual.UserInterface
}
};
- beatmapSets.Clear();
-
for (int i = 0; i < item_count; i++)
{
- beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo().ToLiveUnmanaged());
+ beatmapManager.Import(TestResources.CreateTestBeatmapSetInfo());
}
- first = beatmapSets.First();
-
- playlistOverlay.BeatmapSets.BindTo(beatmapSets);
+ beatmapSets.First().ToLive(Realm);
});
[Test]
@@ -70,9 +80,13 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("wait for animations to complete", () => !playlistOverlay.Transforms.Any());
+ PlaylistItem firstItem = null!;
+
AddStep("hold 1st item handle", () =>
{
- var handle = this.ChildrenOfType>.PlaylistItemHandle>().First();
+ firstItem = this.ChildrenOfType().First();
+ var handle = firstItem.ChildrenOfType().First();
+
InputManager.MoveMouseTo(handle.ScreenSpaceDrawQuad.Centre);
InputManager.PressButton(MouseButton.Left);
});
@@ -83,7 +97,7 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.MoveMouseTo(item.ScreenSpaceDrawQuad.BottomLeft);
});
- AddAssert("song 1 is 5th", () => beatmapSets[4].Equals(first));
+ AddAssert("first is moved", () => playlistOverlay.ChildrenOfType().Single().Items.ElementAt(4).Value.Equals(firstItem.Model.Value));
AddStep("release handle", () => InputManager.ReleaseButton(MouseButton.Left));
}
@@ -101,6 +115,68 @@ namespace osu.Game.Tests.Visual.UserInterface
() => playlistOverlay.ChildrenOfType()
.Where(item => item.MatchingFilter)
.All(item => item.FilterTerms.Any(term => term.ToString().Contains("10"))));
+
+ AddStep("Import new non-matching beatmap", () =>
+ {
+ var testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(1);
+ testBeatmapSetInfo.Beatmaps.Single().Metadata.Title = "no guid";
+ beatmapManager.Import(testBeatmapSetInfo);
+ });
+
+ AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh()));
+
+ AddAssert("results filtered correctly",
+ () => playlistOverlay.ChildrenOfType()
+ .Where(item => item.MatchingFilter)
+ .All(item => item.FilterTerms.Any(term => term.ToString().Contains("10"))));
+ }
+
+ [Test]
+ public void TestCollectionFiltering()
+ {
+ NowPlayingCollectionDropdown collectionDropdown() => playlistOverlay.ChildrenOfType().Single();
+
+ AddStep("Add collection", () =>
+ {
+ Dependencies.Get().Write(r =>
+ {
+ r.RemoveAll();
+ r.Add(new BeatmapCollection("wang"));
+ });
+ });
+
+ AddUntilStep("wait for dropdown to have new collection", () => collectionDropdown().Items.Count() == 2);
+
+ AddStep("Filter to collection", () =>
+ {
+ collectionDropdown().Current.Value = collectionDropdown().Items.Last();
+ });
+
+ AddUntilStep("No items present", () => !playlistOverlay.ChildrenOfType().Any(i => i.MatchingFilter));
+
+ AddStep("Import new non-matching beatmap", () =>
+ {
+ beatmapManager.Import(TestResources.CreateTestBeatmapSetInfo(1));
+ });
+
+ AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh()));
+
+ AddUntilStep("No items matching", () => !playlistOverlay.ChildrenOfType().Any(i => i.MatchingFilter));
+
+ BeatmapSetInfo collectionAddedBeatmapSet = null!;
+
+ AddStep("Import new matching beatmap", () =>
+ {
+ collectionAddedBeatmapSet = TestResources.CreateTestBeatmapSetInfo(1);
+
+ beatmapManager.Import(collectionAddedBeatmapSet);
+ Realm.Write(r => r.All().First().BeatmapMD5Hashes.Add(collectionAddedBeatmapSet.Beatmaps.First().MD5Hash));
+ });
+
+ AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh()));
+
+ AddUntilStep("Only matching item",
+ () => playlistOverlay.ChildrenOfType().Where(i => i.MatchingFilter).Select(i => i.Model.ID), () => Is.EquivalentTo(new[] { collectionAddedBeatmapSet.ID }));
}
}
}
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index 8a358e24d0..ad53f6d90f 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Input.Bindings
private InputManager? parentInputManager;
- public GlobalActionContainer(OsuGameBase game)
+ public GlobalActionContainer(OsuGameBase? game)
: base(matchingMode: KeyCombinationMatchingMode.Modifiers)
{
if (game is IKeyBindingHandler)
@@ -60,6 +60,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay),
new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor),
+ new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ToggleProfile),
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings),
@@ -339,5 +340,8 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleFPSCounter))]
ToggleFPSDisplay,
+
+ [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleProfile))]
+ ToggleProfile,
}
}
diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
index de1a5b189c..172818c1c0 100644
--- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
+++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
@@ -149,6 +149,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString ToggleNotifications => new TranslatableString(getKey(@"toggle_notifications"), @"Toggle notifications");
+ ///
+ /// "Toggle profile"
+ ///
+ public static LocalisableString ToggleProfile => new TranslatableString(getKey(@"toggle_profile"), @"Toggle profile");
+
///
/// "Pause gameplay"
///
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 42133160ca..a0c8e0d555 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -104,131 +104,39 @@ namespace osu.Game.Online.API
///
private int failureCount;
+ ///
+ /// The main API thread loop, which will continue to run until the game is shut down.
+ ///
private void run()
{
while (!cancellationToken.IsCancellationRequested)
{
- switch (State.Value)
+ if (state.Value == APIState.Failing)
{
- case APIState.Failing:
- //todo: replace this with a ping request.
- log.Add(@"In a failing state, waiting a bit before we try again...");
- Thread.Sleep(5000);
+ // To recover from a failing state, falling through and running the full reconnection process seems safest for now.
+ // This could probably be replaced with a ping-style request if we want to avoid the reconnection overheads.
+ log.Add($@"{nameof(APIAccess)} is in a failing state, waiting a bit before we try again...");
+ Thread.Sleep(5000);
+ }
- if (!IsLoggedIn) goto case APIState.Connecting;
+ // Ensure that we have valid credentials.
+ // If not, setting the offline state will allow the game to prompt the user to provide new credentials.
+ if (!HasLogin)
+ {
+ state.Value = APIState.Offline;
+ Thread.Sleep(50);
+ continue;
+ }
- if (queue.Count == 0)
- {
- log.Add(@"Queueing a ping request");
- Queue(new GetUserRequest());
- }
+ Debug.Assert(HasLogin);
- break;
+ // Ensure that we are in an online state. If not, attempt a connect.
+ if (state.Value != APIState.Online)
+ {
+ attemptConnect();
- case APIState.Offline:
- case APIState.Connecting:
- // work to restore a connection...
- if (!HasLogin)
- {
- state.Value = APIState.Offline;
- Thread.Sleep(50);
- continue;
- }
-
- state.Value = APIState.Connecting;
-
- if (localUser.IsDefault)
- {
- // Show a placeholder user if saved credentials are available.
- // This is useful for storing local scores and showing a placeholder username after starting the game,
- // until a valid connection has been established.
- localUser.Value = new APIUser
- {
- Username = ProvidedUsername,
- };
- }
-
- // save the username at this point, if the user requested for it to be.
- config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty);
-
- if (!authentication.HasValidAccessToken)
- {
- LastLoginError = null;
-
- try
- {
- authentication.AuthenticateWithLogin(ProvidedUsername, password);
- }
- catch (Exception e)
- {
- //todo: this fails even on network-related issues. we should probably handle those differently.
- LastLoginError = e;
- log.Add(@"Login failed!");
- password = null;
- authentication.Clear();
- continue;
- }
- }
-
- var userReq = new GetUserRequest();
-
- userReq.Failure += ex =>
- {
- if (ex is APIException)
- {
- LastLoginError = ex;
- log.Add("Login failed on local user retrieval!");
- Logout();
- }
- else if (ex is WebException webException && webException.Message == @"Unauthorized")
- {
- log.Add(@"Login no longer valid");
- Logout();
- }
- else
- failConnectionProcess();
- };
- userReq.Success += u =>
- {
- localUser.Value = u;
-
- // todo: save/pull from settings
- localUser.Value.Status.Value = new UserStatusOnline();
-
- failureCount = 0;
- };
-
- if (!handleRequest(userReq))
- {
- failConnectionProcess();
- continue;
- }
-
- // getting user's friends is considered part of the connection process.
- var friendsReq = new GetFriendsRequest();
-
- friendsReq.Failure += _ => failConnectionProcess();
- friendsReq.Success += res =>
- {
- friends.AddRange(res);
-
- //we're connected!
- state.Value = APIState.Online;
- };
-
- if (!handleRequest(friendsReq))
- {
- failConnectionProcess();
- continue;
- }
-
- // The Success callback event is fired on the main thread, so we should wait for that to run before proceeding.
- // Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests
- // before actually going online.
- while (State.Value > APIState.Offline && State.Value < APIState.Online)
- Thread.Sleep(500);
-
- break;
+ if (state.Value != APIState.Online)
+ continue;
}
// hard bail if we can't get a valid access token.
@@ -238,31 +146,132 @@ namespace osu.Game.Online.API
continue;
}
- while (true)
- {
- APIRequest req;
-
- lock (queue)
- {
- if (queue.Count == 0) break;
-
- req = queue.Dequeue();
- }
-
- handleRequest(req);
- }
-
+ processQueuedRequests();
Thread.Sleep(50);
}
+ }
- void failConnectionProcess()
+ ///
+ /// Dequeue from the queue and run each request synchronously until the queue is empty.
+ ///
+ private void processQueuedRequests()
+ {
+ while (true)
{
- // if something went wrong during the connection process, we want to reset the state (but only if still connecting).
- if (State.Value == APIState.Connecting)
- state.Value = APIState.Failing;
+ APIRequest req;
+
+ lock (queue)
+ {
+ if (queue.Count == 0) return;
+
+ req = queue.Dequeue();
+ }
+
+ handleRequest(req);
}
}
+ ///
+ /// From a non-connected state, perform a full connection flow, obtaining OAuth tokens and populating the local user and friends.
+ ///
+ ///
+ /// This method takes control of and transitions from to either
+ /// - (successful connection)
+ /// - (failed connection but retrying)
+ /// - (failed and can't retry, clear credentials and require user interaction)
+ ///
+ /// Whether the connection attempt was successful.
+ private void attemptConnect()
+ {
+ state.Value = APIState.Connecting;
+
+ if (localUser.IsDefault)
+ {
+ // Show a placeholder user if saved credentials are available.
+ // This is useful for storing local scores and showing a placeholder username after starting the game,
+ // until a valid connection has been established.
+ setLocalUser(new APIUser
+ {
+ Username = ProvidedUsername,
+ });
+ }
+
+ // save the username at this point, if the user requested for it to be.
+ config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty);
+
+ if (!authentication.HasValidAccessToken)
+ {
+ LastLoginError = null;
+
+ try
+ {
+ authentication.AuthenticateWithLogin(ProvidedUsername, password);
+ }
+ catch (Exception e)
+ {
+ //todo: this fails even on network-related issues. we should probably handle those differently.
+ LastLoginError = e;
+ log.Add($@"Login failed for username {ProvidedUsername} ({LastLoginError.Message})!");
+
+ Logout();
+ return;
+ }
+ }
+
+ var userReq = new GetUserRequest();
+ userReq.Failure += ex =>
+ {
+ if (ex is APIException)
+ {
+ LastLoginError = ex;
+ log.Add($@"Login failed for username {ProvidedUsername} on user retrieval ({LastLoginError.Message})!");
+ Logout();
+ }
+ else if (ex is WebException webException && webException.Message == @"Unauthorized")
+ {
+ log.Add(@"Login no longer valid");
+ Logout();
+ }
+ else
+ {
+ state.Value = APIState.Failing;
+ }
+ };
+ userReq.Success += user =>
+ {
+ // todo: save/pull from settings
+ user.Status.Value = new UserStatusOnline();
+
+ setLocalUser(user);
+
+ // we're connected!
+ state.Value = APIState.Online;
+ failureCount = 0;
+ };
+
+ if (!handleRequest(userReq))
+ {
+ state.Value = APIState.Failing;
+ return;
+ }
+
+ var friendsReq = new GetFriendsRequest();
+ friendsReq.Failure += _ => state.Value = APIState.Failing;
+ friendsReq.Success += res => friends.AddRange(res);
+
+ if (!handleRequest(friendsReq))
+ {
+ state.Value = APIState.Failing;
+ return;
+ }
+
+ // The Success callback event is fired on the main thread, so we should wait for that to run before proceeding.
+ // Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests
+ // before actually going online.
+ while (State.Value == APIState.Connecting && !cancellationToken.IsCancellationRequested)
+ Thread.Sleep(500);
+ }
+
public void Perform(APIRequest request)
{
try
@@ -338,8 +347,7 @@ namespace osu.Game.Online.API
if (req.CompletionState != APIRequestCompletionState.Completed)
return false;
- // we could still be in initialisation, at which point we don't want to say we're Online yet.
- if (IsLoggedIn) state.Value = APIState.Online;
+ // Reset failure count if this request succeeded.
failureCount = 0;
return true;
}
@@ -453,7 +461,7 @@ namespace osu.Game.Online.API
// Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present
Schedule(() =>
{
- localUser.Value = createGuestUser();
+ setLocalUser(createGuestUser());
friends.Clear();
});
@@ -463,6 +471,8 @@ namespace osu.Game.Online.API
private static APIUser createGuestUser() => new GuestUser();
+ private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false);
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs
index 812aa7f09f..a90b11e354 100644
--- a/osu.Game/Online/API/IAPIProvider.cs
+++ b/osu.Game/Online/API/IAPIProvider.cs
@@ -13,19 +13,16 @@ namespace osu.Game.Online.API
{
///
/// The local user.
- /// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
///
IBindable LocalUser { get; }
///
/// The user's friends.
- /// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
///
IBindableList Friends { get; }
///
/// The current user's activity.
- /// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
///
IBindable Activity { get; }
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 78cc4d7f70..f7747c5d64 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -1138,6 +1138,13 @@ namespace osu.Game
mouseDisableButtons.Value = !mouseDisableButtons.Value;
return true;
+ case GlobalAction.ToggleProfile:
+ if (userProfile.State.Value == Visibility.Visible)
+ userProfile.Hide();
+ else
+ ShowUser(API.LocalUser.Value);
+ return true;
+
case GlobalAction.RandomSkin:
// Don't allow random skin selection while in the skin editor.
// This is mainly to stop many "osu! default (modified)" skins being created via the SkinManager.EnsureMutableSkin() path.
diff --git a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs
index e94a1b0147..8fc011b2bf 100644
--- a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs
+++ b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs
@@ -4,7 +4,6 @@
#nullable disable
using Markdig.Syntax;
-using Markdig.Syntax.Inlines;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Game.Graphics.Containers.Markdown;
@@ -12,16 +11,8 @@ namespace osu.Game.Overlays.Comments
{
public class CommentMarkdownContainer : OsuMarkdownContainer
{
- public override MarkdownTextFlowContainer CreateTextFlow() => new CommentMarkdownTextFlowContainer();
-
protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new CommentMarkdownHeading(headingBlock);
- private class CommentMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer
- {
- // Don't render image in comment for now
- protected override void AddImage(LinkInline linkInline) { }
- }
-
private class CommentMarkdownHeading : OsuMarkdownHeading
{
public CommentMarkdownHeading(HeadingBlock headingBlock)
diff --git a/osu.Game/Overlays/Music/Playlist.cs b/osu.Game/Overlays/Music/Playlist.cs
index 2bb0ff1085..15fc54a337 100644
--- a/osu.Game/Overlays/Music/Playlist.cs
+++ b/osu.Game/Overlays/Music/Playlist.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Linq;
using osu.Framework.Bindables;
@@ -17,10 +15,12 @@ namespace osu.Game.Overlays.Music
{
public class Playlist : OsuRearrangeableListContainer>
{
- public Action> RequestSelection;
+ public Action>? RequestSelection;
public readonly Bindable> SelectedSet = new Bindable>();
+ private FilterCriteria currentCriteria = new FilterCriteria();
+
public new MarginPadding Padding
{
get => base.Padding;
@@ -31,26 +31,22 @@ namespace osu.Game.Overlays.Music
{
var items = (SearchContainer>>)ListContainer;
- string[] currentCollectionHashes = criteria.Collection?.PerformRead(c => c.BeatmapMD5Hashes.ToArray());
+ string[]? currentCollectionHashes = criteria.Collection?.PerformRead(c => c.BeatmapMD5Hashes.ToArray());
foreach (var item in items.OfType())
{
- if (currentCollectionHashes == null)
- item.InSelectedCollection = true;
- else
- {
- item.InSelectedCollection = item.Model.Value.Beatmaps.Select(b => b.MD5Hash)
- .Any(currentCollectionHashes.Contains);
- }
+ item.InSelectedCollection = currentCollectionHashes == null || item.Model.Value.Beatmaps.Select(b => b.MD5Hash).Any(currentCollectionHashes.Contains);
}
items.SearchTerm = criteria.SearchText;
+ currentCriteria = criteria;
}
- public Live FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter);
+ public Live? FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter);
protected override OsuRearrangeableListItem> CreateOsuDrawable(Live item) => new PlaylistItem(item)
{
+ InSelectedCollection = currentCriteria.Collection?.PerformRead(c => item.Value.Beatmaps.Select(b => b.MD5Hash).Any(c.BeatmapMD5Hashes.Contains)) != false,
SelectedSet = { BindTarget = SelectedSet },
RequestSelection = set => RequestSelection?.Invoke(set)
};
diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs
index e33fc8064f..9fe2fd5279 100644
--- a/osu.Game/Overlays/Music/PlaylistOverlay.cs
+++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs
@@ -26,8 +26,6 @@ namespace osu.Game.Overlays.Music
private const float transition_duration = 600;
private const float playlist_height = 510;
- public IBindableList> BeatmapSets => beatmapSets;
-
private readonly BindableList> beatmapSets = new BindableList>();
private readonly Bindable beatmap = new Bindable();
@@ -104,9 +102,7 @@ namespace osu.Game.Overlays.Music
{
base.LoadComplete();
- // tests might bind externally, in which case we don't want to involve realm.
- if (beatmapSets.Count == 0)
- beatmapSubscription = realm.RegisterForNotifications(r => r.All().Where(s => !s.DeletePending), beatmapsChanged);
+ beatmapSubscription = realm.RegisterForNotifications(r => r.All().Where(s => !s.DeletePending), beatmapsChanged);
list.Items.BindTo(beatmapSets);
beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo.ToLive(realm), true);
diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs
index e7996c6d43..697b303689 100644
--- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs
+++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.Fun;
- public override double ScoreMultiplier => 1;
+ public override double ScoreMultiplier => 0.5;
public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false;
diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
index 4ed31eec78..f4c6be4f77 100644
--- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
+++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => FontAwesome.Solid.Hammer;
- public override double ScoreMultiplier => 1.0;
+ public override double ScoreMultiplier => 0.5;
public override bool RequiresConfiguration => true;
diff --git a/osu.Game/Rulesets/Mods/ModRelax.cs b/osu.Game/Rulesets/Mods/ModRelax.cs
index e5995ff180..49c10339ee 100644
--- a/osu.Game/Rulesets/Mods/ModRelax.cs
+++ b/osu.Game/Rulesets/Mods/ModRelax.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mods
public override string Acronym => "RX";
public override IconUsage? Icon => OsuIcon.ModRelax;
public override ModType Type => ModType.Automation;
- public override double ScoreMultiplier => 1;
+ public override double ScoreMultiplier => 0.1;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModNoFail), typeof(ModFailCondition) };
}
}
diff --git a/osu.Game/Tests/VisualTestRunner.cs b/osu.Game/Tests/VisualTestRunner.cs
index bd98482768..c8279b9e3c 100644
--- a/osu.Game/Tests/VisualTestRunner.cs
+++ b/osu.Game/Tests/VisualTestRunner.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Tests
[STAThread]
public static int Main(string[] args)
{
- using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true, }))
+ using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu-development", new HostOptions { BindIPC = true, }))
{
host.Run(new OsuTestBrowser());
return 0;