diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml
index d4150208d3..5f16e09040 100644
--- a/.github/workflows/diffcalc.yml
+++ b/.github/workflows/diffcalc.yml
@@ -189,8 +189,8 @@ jobs:
COMMENT_BODY: ${{ github.event.comment.body }}
run: |
# Add comment environment
- echo $COMMENT_BODY | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do
- opt=$(echo ${line} | cut -d '=' -f1)
+ echo "$COMMENT_BODY" | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do
+ opt=$(echo "${line}" | cut -d '=' -f1)
sed -i "s;^${opt}=.*$;${line};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
done
diff --git a/osu.Android.props b/osu.Android.props
index 3b90b1675c..6609db3027 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -10,7 +10,7 @@
true
-
+
-
\ No newline at end of file
+
+
+
diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
index 9323296b7f..66c76f9b17 100644
--- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
@@ -4,11 +4,19 @@
using System;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
namespace osu.Game.Rulesets.Catch.Scoring
{
public partial class CatchScoreProcessor : ScoreProcessor
{
+ private const double accuracy_cutoff_x = 1;
+ private const double accuracy_cutoff_s = 0.98;
+ private const double accuracy_cutoff_a = 0.94;
+ private const double accuracy_cutoff_b = 0.9;
+ private const double accuracy_cutoff_c = 0.85;
+ private const double accuracy_cutoff_d = 0;
+
private const int combo_cap = 200;
private const double combo_base = 4;
@@ -26,5 +34,50 @@ namespace osu.Game.Rulesets.Catch.Scoring
protected override double GetComboScoreChange(JudgementResult result)
=> Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
+
+ public override ScoreRank RankFromAccuracy(double accuracy)
+ {
+ if (accuracy == accuracy_cutoff_x)
+ return ScoreRank.X;
+ if (accuracy >= accuracy_cutoff_s)
+ return ScoreRank.S;
+ if (accuracy >= accuracy_cutoff_a)
+ return ScoreRank.A;
+ if (accuracy >= accuracy_cutoff_b)
+ return ScoreRank.B;
+ if (accuracy >= accuracy_cutoff_c)
+ return ScoreRank.C;
+
+ return ScoreRank.D;
+ }
+
+ public override double AccuracyCutoffFromRank(ScoreRank rank)
+ {
+ switch (rank)
+ {
+ case ScoreRank.X:
+ case ScoreRank.XH:
+ return accuracy_cutoff_x;
+
+ case ScoreRank.S:
+ case ScoreRank.SH:
+ return accuracy_cutoff_s;
+
+ case ScoreRank.A:
+ return accuracy_cutoff_a;
+
+ case ScoreRank.B:
+ return accuracy_cutoff_b;
+
+ case ScoreRank.C:
+ return accuracy_cutoff_c;
+
+ case ScoreRank.D:
+ return accuracy_cutoff_d;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(rank), rank, null);
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs
new file mode 100644
index 0000000000..315849f7de
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs
@@ -0,0 +1,31 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Scoring;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ [TestFixture]
+ public class ManiaHealthProcessorTest
+ {
+ [Test]
+ public void TestNoDrain()
+ {
+ var processor = new ManiaHealthProcessor(0);
+ processor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
+ {
+ HitObjects =
+ {
+ new Note { StartTime = 0 },
+ new Note { StartTime = 1000 },
+ }
+ });
+
+ // No matter what, mania doesn't have passive HP drain.
+ Assert.That(processor.DrainRate, Is.Zero);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index aaef69f119..ccfe1501bd 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -174,9 +174,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
switch (original)
{
- case IHasDistance:
+ case IHasPath:
{
- var generator = new DistanceObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
+ var generator = new PathObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
conversion = generator;
var positionData = original as IHasPosition;
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs
similarity index 96%
rename from osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
rename to osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs
index cce0944564..4922915c7d 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs
@@ -22,13 +22,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
/// A pattern generator for IHasDistance hit objects.
///
- internal class DistanceObjectPatternGenerator : PatternGenerator
+ internal class PathObjectPatternGenerator : PatternGenerator
{
- ///
- /// Base osu! slider scoring distance.
- ///
- private const float osu_base_scoring_distance = 100;
-
public readonly int StartTime;
public readonly int EndTime;
public readonly int SegmentDuration;
@@ -36,17 +31,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
private PatternType convertType;
- public DistanceObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
+ public PathObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{
convertType = PatternType.None;
if (!Beatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode)
convertType = PatternType.LowProbability;
- var distanceData = hitObject as IHasDistance;
+ var pathData = hitObject as IHasPath;
var repeatsData = hitObject as IHasRepeats;
- Debug.Assert(distanceData != null);
+ Debug.Assert(pathData != null);
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
@@ -60,8 +55,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
SpanCount = repeatsData?.SpanCount() ?? 1;
StartTime = (int)Math.Round(hitObject.StartTime);
+ double distance = pathData.Path.ExpectedDistance.Value ?? 0;
+
// This matches stable's calculation.
- EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.Difficulty.SliderMultiplier);
+ EndTime = (int)Math.Floor(StartTime + distance * beatLength * SpanCount * 0.01 / beatmap.Difficulty.SliderMultiplier);
SegmentDuration = (EndTime - StartTime) / SpanCount;
}
diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs
index 183550eb7b..a33eac83c2 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs
@@ -15,6 +15,15 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
}
+ protected override double ComputeDrainRate()
+ {
+ // Base call is run only to compute HP recovery (namely, `HpMultiplierNormal`).
+ // This closely mirrors (broken) behaviour of stable and as such is preserved unchanged.
+ base.ComputeDrainRate();
+
+ return 0;
+ }
+
protected override IEnumerable EnumerateTopLevelHitObjects() => Beatmap.HitObjects;
protected override IEnumerable EnumerateNestedHitObjects(HitObject hitObject) => hitObject.NestedHitObjects;
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index e46e2ec09c..2551321ff2 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -109,9 +109,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
switch (obj)
{
- case IHasDistance distanceData:
+ case IHasPath pathData:
{
- if (shouldConvertSliderToHits(obj, beatmap, distanceData, out int taikoDuration, out double tickSpacing))
+ if (shouldConvertSliderToHits(obj, beatmap, pathData, out int taikoDuration, out double tickSpacing))
{
IList> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List>(new[] { samples });
@@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
}
}
- private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDistance distanceData, out int taikoDuration, out double tickSpacing)
+ private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasPath pathData, out int taikoDuration, out double tickSpacing)
{
// DO NOT CHANGE OR REFACTOR ANYTHING IN HERE WITHOUT TESTING AGAINST _ALL_ BEATMAPS.
// Some of these calculations look redundant, but they are not - extremely small floating point errors are introduced to maintain 1:1 compatibility with stable.
@@ -182,7 +182,11 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
// The true distance, accounting for any repeats. This ends up being the drum roll distance later
int spans = (obj as IHasRepeats)?.SpanCount() ?? 1;
- double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
+ double distance = pathData.Path.ExpectedDistance.Value ?? 0;
+
+ // Do not combine the following two lines!
+ distance *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
+ distance *= spans;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index dcfe8ecb41..02432a1935 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -15,12 +15,14 @@ using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Catch.Beatmaps;
+using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Rulesets.Taiko;
using osu.Game.Skinning;
using osu.Game.Tests.Resources;
using osuTK;
@@ -1156,5 +1158,35 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(((IHasComboInformation)playable.HitObjects[17]).ComboIndexWithOffsets, Is.EqualTo(9));
}
}
+
+ [Test]
+ public void TestSliderConversionWithCustomDistance([Values("taiko", "mania")] string rulesetName)
+ {
+ using (var resStream = TestResources.OpenResource("custom-slider-length.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ Ruleset ruleset;
+
+ switch (rulesetName)
+ {
+ case "taiko":
+ ruleset = new TaikoRuleset();
+ break;
+
+ case "mania":
+ ruleset = new ManiaRuleset();
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(rulesetName), rulesetName, null);
+ }
+
+ var decoder = Decoder.GetDecoder(stream);
+ var working = new TestWorkingBeatmap(decoder.Decode(stream));
+ IBeatmap beatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo, Array.Empty());
+
+ Assert.That(beatmap.HitObjects[0].GetEndTime(), Is.EqualTo(3153));
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Resources/custom-slider-length.osu b/osu.Game.Tests/Resources/custom-slider-length.osu
new file mode 100644
index 0000000000..f7529918a9
--- /dev/null
+++ b/osu.Game.Tests/Resources/custom-slider-length.osu
@@ -0,0 +1,19 @@
+osu file format v14
+
+[General]
+Mode: 0
+
+[Difficulty]
+HPDrainRate:6
+CircleSize:7
+OverallDifficulty:7
+ApproachRate:10
+SliderMultiplier:1.7
+SliderTickRate:1
+
+[TimingPoints]
+29,333.333333333333,4,1,0,100,1,0
+29,-10000,4,1,0,100,0,0
+
+[HitObjects]
+256,192,29,6,0,P|384:192|384:192,1,159.375
\ No newline at end of file
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
index 6d309078e6..2d61c26a6b 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
@@ -378,6 +378,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
}, users);
}
+ [Test]
+ public void TestAbortMatch()
+ {
+ AddStep("setup client", () =>
+ {
+ multiplayerClient.Setup(m => m.StartMatch())
+ .Callback(() =>
+ {
+ multiplayerClient.Raise(m => m.LoadRequested -= null);
+ multiplayerClient.Object.Room!.State = MultiplayerRoomState.WaitingForLoad;
+
+ // The local user state doesn't really matter, so let's do the same as the base implementation for these tests.
+ changeUserState(localUser.UserID, MultiplayerUserState.Idle);
+ });
+
+ multiplayerClient.Setup(m => m.AbortMatch())
+ .Callback(() =>
+ {
+ multiplayerClient.Object.Room!.State = MultiplayerRoomState.Open;
+ raiseRoomUpdated();
+ });
+ });
+
+ // Ready
+ ClickButtonWhenEnabled();
+
+ // Start match
+ ClickButtonWhenEnabled();
+ AddUntilStep("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value);
+
+ // Abort
+ ClickButtonWhenEnabled();
+ AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once));
+ }
+
private void verifyGameplayStartFlow()
{
checkLocalUserState(MultiplayerUserState.Ready);
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs
index c17a9ddf5f..fa85c8c9f8 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs
@@ -12,9 +12,12 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Threading;
+using osu.Game.Online.API;
+using osu.Game.Beatmaps;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Play;
@@ -38,6 +41,9 @@ namespace osu.Game.Tests.Visual.Navigation
advanceToSongSelect();
openSkinEditor();
+ AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
+ AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
+
switchToGameplayScene();
BarHitErrorMeter hitErrorMeter = null;
@@ -98,6 +104,10 @@ namespace osu.Game.Tests.Visual.Navigation
{
advanceToSongSelect();
openSkinEditor();
+
+ AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
+ AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
+
switchToGameplayScene();
AddUntilStep("wait for components", () => skinEditor.ChildrenOfType().Any());
@@ -162,6 +172,9 @@ namespace osu.Game.Tests.Visual.Navigation
openSkinEditor();
AddStep("select DT", () => Game.SelectedMods.Value = new Mod[] { new OsuModDoubleTime() });
+ AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
+ AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
+
switchToGameplayScene();
AddAssert("DT still selected", () => ((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Single() is OsuModDoubleTime);
@@ -174,6 +187,9 @@ namespace osu.Game.Tests.Visual.Navigation
openSkinEditor();
AddStep("select relax and spun out", () => Game.SelectedMods.Value = new Mod[] { new OsuModRelax(), new OsuModSpunOut() });
+ AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
+ AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
+
switchToGameplayScene();
AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any());
@@ -186,6 +202,9 @@ namespace osu.Game.Tests.Visual.Navigation
openSkinEditor();
AddStep("select autoplay", () => Game.SelectedMods.Value = new Mod[] { new OsuModAutoplay() });
+ AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
+ AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
+
switchToGameplayScene();
AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any());
@@ -198,6 +217,9 @@ namespace osu.Game.Tests.Visual.Navigation
openSkinEditor();
AddStep("select cinema", () => Game.SelectedMods.Value = new Mod[] { new OsuModCinema() });
+ AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
+ AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
+
switchToGameplayScene();
AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any());
@@ -240,6 +262,43 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("editor sidebars not empty", () => skinEditor.ChildrenOfType().SelectMany(sidebar => sidebar.Children).Count(), () => Is.GreaterThan(0));
}
+ [Test]
+ public void TestOpenSkinEditorGameplaySceneOnBeatmapWithNoObjects()
+ {
+ AddStep("set dummy beatmap", () => Game.Beatmap.SetDefault());
+ advanceToSongSelect();
+
+ AddStep("create empty beatmap", () => Game.BeatmapManager.CreateNew(new OsuRuleset().RulesetInfo, new GuestUser()));
+ AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
+
+ openSkinEditor();
+ switchToGameplayScene();
+ }
+
+ [Test]
+ public void TestOpenSkinEditorGameplaySceneWhenDummyBeatmapActive()
+ {
+ AddStep("set dummy beatmap", () => Game.Beatmap.SetDefault());
+
+ openSkinEditor();
+ }
+
+ [Test]
+ public void TestOpenSkinEditorGameplaySceneWhenDifferentRulesetActive()
+ {
+ BeatmapSetInfo beatmapSet = null!;
+
+ AddStep("import beatmap", () => beatmapSet = BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely());
+ AddStep("select mania difficulty", () =>
+ {
+ var beatmap = beatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 3);
+ Game.Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(beatmap);
+ });
+
+ openSkinEditor();
+ switchToGameplayScene();
+ }
+
private void advanceToSongSelect()
{
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
@@ -266,9 +325,6 @@ namespace osu.Game.Tests.Visual.Navigation
private void switchToGameplayScene()
{
- AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
- AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
-
AddStep("Click gameplay scene button", () =>
{
InputManager.MoveMouseTo(skinEditor.ChildrenOfType().First(b => b.Text.ToString() == "Gameplay"));
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
index 7616b9b83c..c793535255 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.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 NUnit.Framework;
@@ -56,92 +54,76 @@ namespace osu.Game.Tests.Visual.Online
textContainer.Clear();
});
- [Test]
- public void TestLinksGeneral()
+ [TestCase("test!")]
+ [TestCase("dev.ppy.sh!")]
+ [TestCase("https://dev.ppy.sh!", LinkAction.External)]
+ [TestCase("http://dev.ppy.sh!", LinkAction.External)]
+ [TestCase("forgothttps://dev.ppy.sh!", LinkAction.External)]
+ [TestCase("forgothttp://dev.ppy.sh!", LinkAction.External)]
+ [TestCase("00:12:345 - Test?", LinkAction.OpenEditorTimestamp)]
+ [TestCase("00:12:345 (1,2) - Test?", LinkAction.OpenEditorTimestamp)]
+ [TestCase($"{OsuGameBase.OSU_PROTOCOL}edit/00:12:345 - Test?", LinkAction.OpenEditorTimestamp)]
+ [TestCase($"{OsuGameBase.OSU_PROTOCOL}edit/00:12:345 (1,2) - Test?", LinkAction.OpenEditorTimestamp)]
+ [TestCase($"{OsuGameBase.OSU_PROTOCOL}00:12:345 - not an editor timestamp", LinkAction.External)]
+ [TestCase("Wiki link for tasty [[Performance Points]]", LinkAction.OpenWiki)]
+ [TestCase("(osu forums)[https://dev.ppy.sh/forum] (old link format)", LinkAction.External)]
+ [TestCase("[https://dev.ppy.sh/home New site] (new link format)", LinkAction.External)]
+ [TestCase("[osu forums](https://dev.ppy.sh/forum) (new link format 2)", LinkAction.External)]
+ [TestCase("[https://dev.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", LinkAction.External)]
+ [TestCase("Let's (try)[https://dev.ppy.sh/home] [https://dev.ppy.sh/b/252238 multiple links] https://dev.ppy.sh/home", LinkAction.External, LinkAction.OpenBeatmap, LinkAction.External)]
+ [TestCase("[https://dev.ppy.sh/home New link format with escaped [and \\[ paired] braces]", LinkAction.External)]
+ [TestCase("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", LinkAction.External)]
+ [TestCase("(Old link format with escaped (and \\( paired) parentheses)[https://dev.ppy.sh/home] and [[also a rogue wiki link]]", LinkAction.External, LinkAction.OpenWiki)]
+ [TestCase("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present).")] // note that there's 0 links here (they get removed if a channel is not found)
+ [TestCase("Join my multiplayer game osump://12346.", LinkAction.JoinMultiplayerMatch)]
+ [TestCase("Join my multiplayer gameosump://12346.", LinkAction.JoinMultiplayerMatch)]
+ [TestCase("Join my [multiplayer game](osump://12346).", LinkAction.JoinMultiplayerMatch)]
+ [TestCase($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", LinkAction.OpenChannel)]
+ [TestCase($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)]
+ [TestCase($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)]
+ [TestCase("Join my #english or #japanese channels.", LinkAction.OpenChannel, LinkAction.OpenChannel)]
+ [TestCase("Join my #english or #nonexistent #hashtag channels.", LinkAction.OpenChannel)]
+ [TestCase("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20")]
+ public void TestLinksGeneral(string text, params LinkAction[] actions)
{
- int messageIndex = 0;
+ addMessageWithChecks(text, expectedActions: actions);
+ }
- addMessageWithChecks("test!");
- addMessageWithChecks("dev.ppy.sh!");
- addMessageWithChecks("https://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
- addMessageWithChecks("http://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
- addMessageWithChecks("forgothttps://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
- addMessageWithChecks("forgothttp://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
- addMessageWithChecks("00:12:345 (1,2) - Test?", 1, expectedActions: LinkAction.OpenEditorTimestamp);
- addMessageWithChecks("Wiki link for tasty [[Performance Points]]", 1, expectedActions: LinkAction.OpenWiki);
- addMessageWithChecks("(osu forums)[https://dev.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External);
- addMessageWithChecks("[https://dev.ppy.sh/home New site] (new link format)", 1, expectedActions: LinkAction.External);
- addMessageWithChecks("[osu forums](https://dev.ppy.sh/forum) (new link format 2)", 1, expectedActions: LinkAction.External);
- addMessageWithChecks("[https://dev.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", 1, expectedActions: LinkAction.External);
- addMessageWithChecks("is now listening to [https://dev.ppy.sh/s/93523 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmapSet);
- addMessageWithChecks("is now playing [https://dev.ppy.sh/b/252238 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmap);
- addMessageWithChecks("Let's (try)[https://dev.ppy.sh/home] [https://dev.ppy.sh/b/252238 multiple links] https://dev.ppy.sh/home", 3,
- expectedActions: new[] { LinkAction.External, LinkAction.OpenBeatmap, LinkAction.External });
- addMessageWithChecks("[https://dev.ppy.sh/home New link format with escaped [and \\[ paired] braces]", 1, expectedActions: LinkAction.External);
- addMessageWithChecks("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", 1, expectedActions: LinkAction.External);
- addMessageWithChecks("(Old link format with escaped (and \\( paired) parentheses)[https://dev.ppy.sh/home] and [[also a rogue wiki link]]", 2,
- expectedActions: new[] { LinkAction.External, LinkAction.OpenWiki });
- // note that there's 0 links here (they get removed if a channel is not found)
- addMessageWithChecks("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present).");
- addMessageWithChecks("I am important!", 0, false, true);
- addMessageWithChecks("feels important", 0, true, true);
- addMessageWithChecks("likes to post this [https://dev.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External);
- addMessageWithChecks("Join my multiplayer game osump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
- addMessageWithChecks("Join my multiplayer gameosump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
- addMessageWithChecks("Join my [multiplayer game](osump://12346).", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
- addMessageWithChecks($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", 1, expectedActions: LinkAction.OpenChannel);
- addMessageWithChecks($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel);
- addMessageWithChecks($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel);
- addMessageWithChecks("Join my #english or #japanese channels.", 2, expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel });
- addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", 1, expectedActions: LinkAction.OpenChannel);
- addMessageWithChecks("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20");
+ [TestCase("is now listening to [https://dev.ppy.sh/s/93523 IMAGE -MATERIAL- ]", true, false, LinkAction.OpenBeatmapSet)]
+ [TestCase("is now playing [https://dev.ppy.sh/b/252238 IMAGE -MATERIAL- ]", true, false, LinkAction.OpenBeatmap)]
+ [TestCase("I am important!", false, true)]
+ [TestCase("feels important", true, true)]
+ [TestCase("likes to post this [https://dev.ppy.sh/home link].", true, true, LinkAction.External)]
+ public void TestActionAndImportantLinks(string text, bool isAction, bool isImportant, params LinkAction[] expectedActions)
+ {
+ addMessageWithChecks(text, isAction, isImportant, expectedActions);
+ }
- void addMessageWithChecks(string text, int linkAmount = 0, bool isAction = false, bool isImportant = false, params LinkAction[] expectedActions)
+ private void addMessageWithChecks(string text, bool isAction = false, bool isImportant = false, params LinkAction[] expectedActions)
+ {
+ ChatLine newLine = null!;
+
+ AddStep("add message", () =>
{
- ChatLine newLine = null;
- int index = messageIndex++;
+ newLine = new ChatLine(new DummyMessage(text, isAction, isImportant));
+ textContainer.Add(newLine);
+ });
- AddStep("add message", () =>
- {
- newLine = new ChatLine(new DummyMessage(text, isAction, isImportant, index));
- textContainer.Add(newLine);
- });
+ AddAssert("msg has the right action", () => newLine.Message.Links.Select(l => l.Action), () => Is.EqualTo(expectedActions));
+ AddAssert($"msg shows {expectedActions.Length} link(s)", isShowingLinks);
- AddAssert($"msg #{index} has {linkAmount} link(s)", () => newLine.Message.Links.Count == linkAmount);
- AddAssert($"msg #{index} has the right action", hasExpectedActions);
- //AddAssert($"msg #{index} is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic());
- AddAssert($"msg #{index} shows {linkAmount} link(s)", isShowingLinks);
+ bool isShowingLinks()
+ {
+ bool hasBackground = !string.IsNullOrEmpty(newLine.Message.Sender.Colour);
- bool hasExpectedActions()
- {
- var expectedActionsList = expectedActions.ToList();
+ Color4 textColour = isAction && hasBackground ? Color4Extensions.FromHex(newLine.Message.Sender.Colour) : Color4.White;
- if (expectedActionsList.Count != newLine.Message.Links.Count)
- return false;
+ var linkCompilers = newLine.DrawableContentFlow.Where(d => d is DrawableLinkCompiler).ToList();
+ var linkSprites = linkCompilers.SelectMany(comp => ((DrawableLinkCompiler)comp).Parts);
- for (int i = 0; i < newLine.Message.Links.Count; i++)
- {
- var action = newLine.Message.Links[i].Action;
- if (action != expectedActions[i]) return false;
- }
-
- return true;
- }
-
- //bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast().All(sprite => sprite.Font.Italics);
-
- bool isShowingLinks()
- {
- bool hasBackground = !string.IsNullOrEmpty(newLine.Message.Sender.Colour);
-
- Color4 textColour = isAction && hasBackground ? Color4Extensions.FromHex(newLine.Message.Sender.Colour) : Color4.White;
-
- var linkCompilers = newLine.DrawableContentFlow.Where(d => d is DrawableLinkCompiler).ToList();
- var linkSprites = linkCompilers.SelectMany(comp => ((DrawableLinkCompiler)comp).Parts);
-
- return linkSprites.All(d => d.Colour == linkColour)
- && newLine.DrawableContentFlow.Except(linkSprites.Concat(linkCompilers)).All(d => d.Colour == textColour);
- }
+ return linkSprites.All(d => d.Colour == linkColour)
+ && newLine.DrawableContentFlow.Except(linkSprites.Concat(linkCompilers)).All(d => d.Colour == textColour)
+ && linkCompilers.Count == expectedActions.Length;
}
}
@@ -155,7 +137,7 @@ namespace osu.Game.Tests.Visual.Online
addEchoWithWait("[https://dev.ppy.sh/forum let's try multiple words too!]");
addEchoWithWait("(long loading times! clickable while loading?)[https://dev.ppy.sh/home]", null, 5000);
- void addEchoWithWait(string text, string completeText = null, double delay = 250)
+ void addEchoWithWait(string text, string? completeText = null, double delay = 250)
{
int index = messageIndex++;
@@ -184,21 +166,12 @@ namespace osu.Game.Tests.Visual.Online
{
private static long messageCounter;
- internal static readonly APIUser TEST_SENDER_BACKGROUND = new APIUser
- {
- Username = @"i-am-important",
- Id = 42,
- Colour = "#250cc9",
- };
-
internal static readonly APIUser TEST_SENDER = new APIUser
{
Username = @"Somebody",
Id = 1,
};
- public new DateTimeOffset Timestamp = DateTimeOffset.Now;
-
public DummyMessage(string text, bool isAction = false, bool isImportant = false, int number = 0)
: base(messageCounter++)
{
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
index 03b168c72c..435dd77120 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
@@ -9,6 +9,8 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
@@ -22,31 +24,48 @@ namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneAccuracyCircle : OsuTestScene
{
- [TestCase(0)]
- [TestCase(0.2)]
- [TestCase(0.5)]
- [TestCase(0.6999)]
- [TestCase(0.7)]
- [TestCase(0.75)]
- [TestCase(0.7999)]
- [TestCase(0.8)]
- [TestCase(0.85)]
- [TestCase(0.8999)]
- [TestCase(0.9)]
- [TestCase(0.925)]
- [TestCase(0.9499)]
- [TestCase(0.95)]
- [TestCase(0.975)]
- [TestCase(0.9999)]
- [TestCase(1)]
- public void TestRank(double accuracy)
+ [Test]
+ public void TestOsuRank()
{
- var score = createScore(accuracy, ScoreProcessor.RankFromAccuracy(accuracy));
-
- addCircleStep(score);
+ addCircleStep(createScore(0, new OsuRuleset()));
+ addCircleStep(createScore(0.5, new OsuRuleset()));
+ addCircleStep(createScore(0.699, new OsuRuleset()));
+ addCircleStep(createScore(0.7, new OsuRuleset()));
+ addCircleStep(createScore(0.75, new OsuRuleset()));
+ addCircleStep(createScore(0.799, new OsuRuleset()));
+ addCircleStep(createScore(0.8, new OsuRuleset()));
+ addCircleStep(createScore(0.85, new OsuRuleset()));
+ addCircleStep(createScore(0.899, new OsuRuleset()));
+ addCircleStep(createScore(0.9, new OsuRuleset()));
+ addCircleStep(createScore(0.925, new OsuRuleset()));
+ addCircleStep(createScore(0.9499, new OsuRuleset()));
+ addCircleStep(createScore(0.95, new OsuRuleset()));
+ addCircleStep(createScore(0.975, new OsuRuleset()));
+ addCircleStep(createScore(0.99, new OsuRuleset()));
+ addCircleStep(createScore(1, new OsuRuleset()));
}
- private void addCircleStep(ScoreInfo score) => AddStep("add panel", () =>
+ [Test]
+ public void TestCatchRank()
+ {
+ addCircleStep(createScore(0, new CatchRuleset()));
+ addCircleStep(createScore(0.5, new CatchRuleset()));
+ addCircleStep(createScore(0.8499, new CatchRuleset()));
+ addCircleStep(createScore(0.85, new CatchRuleset()));
+ addCircleStep(createScore(0.875, new CatchRuleset()));
+ addCircleStep(createScore(0.899, new CatchRuleset()));
+ addCircleStep(createScore(0.9, new CatchRuleset()));
+ addCircleStep(createScore(0.925, new CatchRuleset()));
+ addCircleStep(createScore(0.9399, new CatchRuleset()));
+ addCircleStep(createScore(0.94, new CatchRuleset()));
+ addCircleStep(createScore(0.9675, new CatchRuleset()));
+ addCircleStep(createScore(0.9799, new CatchRuleset()));
+ addCircleStep(createScore(0.98, new CatchRuleset()));
+ addCircleStep(createScore(0.99, new CatchRuleset()));
+ addCircleStep(createScore(1, new CatchRuleset()));
+ }
+
+ private void addCircleStep(ScoreInfo score) => AddStep($"add panel ({score.DisplayAccuracy})", () =>
{
Children = new Drawable[]
{
@@ -73,28 +92,33 @@ namespace osu.Game.Tests.Visual.Ranking
};
});
- private ScoreInfo createScore(double accuracy, ScoreRank rank) => new ScoreInfo
+ private ScoreInfo createScore(double accuracy, Ruleset ruleset)
{
- User = new APIUser
+ var scoreProcessor = ruleset.CreateScoreProcessor();
+
+ return new ScoreInfo
{
- Id = 2,
- Username = "peppy",
- },
- BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo,
- Ruleset = new OsuRuleset().RulesetInfo,
- Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
- TotalScore = 2845370,
- Accuracy = accuracy,
- MaxCombo = 999,
- Rank = rank,
- Date = DateTimeOffset.Now,
- Statistics =
- {
- { HitResult.Miss, 1 },
- { HitResult.Meh, 50 },
- { HitResult.Good, 100 },
- { HitResult.Great, 300 },
- }
- };
+ User = new APIUser
+ {
+ Id = 2,
+ Username = "peppy",
+ },
+ BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo,
+ Ruleset = ruleset.RulesetInfo,
+ Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
+ TotalScore = 2845370,
+ Accuracy = accuracy,
+ MaxCombo = 999,
+ Rank = scoreProcessor.RankFromAccuracy(accuracy),
+ Date = DateTimeOffset.Now,
+ Statistics =
+ {
+ { HitResult.Miss, 1 },
+ { HitResult.Meh, 50 },
+ { HitResult.Good, 100 },
+ { HitResult.Great, 300 },
+ }
+ };
+ }
}
}
diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs
index 3aab9a24e1..b00d0ba316 100644
--- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs
+++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs
@@ -1,8 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Collections.Generic;
using osu.Framework.Localisation;
+using osu.Game.Screens.Select;
namespace osu.Game.Beatmaps
{
@@ -29,20 +29,22 @@ namespace osu.Game.Beatmaps
return new RomanisableString($"{metadata.GetPreferred(true)}".Trim(), $"{metadata.GetPreferred(false)}".Trim());
}
- public static List GetSearchableTerms(this IBeatmapInfo beatmapInfo)
+ public static bool Match(this IBeatmapInfo beatmapInfo, params FilterCriteria.OptionalTextFilter[] filters)
{
- var termsList = new List(BeatmapMetadataInfoExtensions.MAX_SEARCHABLE_TERM_COUNT + 1);
-
- addIfNotNull(beatmapInfo.DifficultyName);
-
- BeatmapMetadataInfoExtensions.CollectSearchableTerms(beatmapInfo.Metadata, termsList);
- return termsList;
-
- void addIfNotNull(string? s)
+ foreach (var filter in filters)
{
- if (!string.IsNullOrEmpty(s))
- termsList.Add(s);
+ if (filter.Matches(beatmapInfo.DifficultyName))
+ continue;
+
+ if (BeatmapMetadataInfoExtensions.Match(beatmapInfo.Metadata, filter))
+ continue;
+
+ // failed to match a single filter at all - fail the whole match.
+ return false;
}
+
+ // got through all filters without failing any - pass the whole match.
+ return true;
}
private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]";
diff --git a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs
index be96a66614..198469dba6 100644
--- a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs
+++ b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs
@@ -3,11 +3,14 @@
using System.Collections.Generic;
using osu.Framework.Localisation;
+using osu.Game.Screens.Select;
namespace osu.Game.Beatmaps
{
public static class BeatmapMetadataInfoExtensions
{
+ internal const int MAX_SEARCHABLE_TERM_COUNT = 7;
+
///
/// An array of all searchable terms provided in contained metadata.
///
@@ -18,7 +21,18 @@ namespace osu.Game.Beatmaps
return termsList.ToArray();
}
- internal const int MAX_SEARCHABLE_TERM_COUNT = 7;
+ public static bool Match(IBeatmapMetadataInfo metadataInfo, FilterCriteria.OptionalTextFilter filter)
+ {
+ if (filter.Matches(metadataInfo.Author.Username)) return true;
+ if (filter.Matches(metadataInfo.Artist)) return true;
+ if (filter.Matches(metadataInfo.ArtistUnicode)) return true;
+ if (filter.Matches(metadataInfo.Title)) return true;
+ if (filter.Matches(metadataInfo.TitleUnicode)) return true;
+ if (filter.Matches(metadataInfo.Source)) return true;
+ if (filter.Matches(metadataInfo.Tags)) return true;
+
+ return false;
+ }
internal static void CollectSearchableTerms(IBeatmapMetadataInfo metadataInfo, IList termsList)
{
diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs
index 40e883f8ac..aa72996fff 100644
--- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs
+++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs
@@ -12,6 +12,7 @@ using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
+using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Online;
using osu.Game.Users;
@@ -47,9 +48,16 @@ namespace osu.Game.Graphics.Containers
foreach (var link in links)
{
+ string displayText = text.Substring(link.Index, link.Length);
+
+ if (previousLinkEnd > link.Index)
+ {
+ Logger.Log($@"Link ""{link.Url}"" with text ""{displayText}"" overlaps previous link, ignoring.");
+ continue;
+ }
+
AddText(text[previousLinkEnd..link.Index]);
- string displayText = text.Substring(link.Index, link.Length);
object linkArgument = link.Argument;
string tooltip = displayText == link.Url ? null : link.Url;
diff --git a/osu.Game/Localisation/EditorSetupStrings.cs b/osu.Game/Localisation/EditorSetupStrings.cs
index 401411365b..eff6f9e6b8 100644
--- a/osu.Game/Localisation/EditorSetupStrings.cs
+++ b/osu.Game/Localisation/EditorSetupStrings.cs
@@ -179,21 +179,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString ClickToSelectTrack => new TranslatableString(getKey(@"click_to_select_track"), @"Click to select a track");
- ///
- /// "Click to replace the track"
- ///
- public static LocalisableString ClickToReplaceTrack => new TranslatableString(getKey(@"click_to_replace_track"), @"Click to replace the track");
-
///
/// "Click to select a background image"
///
public static LocalisableString ClickToSelectBackground => new TranslatableString(getKey(@"click_to_select_background"), @"Click to select a background image");
- ///
- /// "Click to replace the background image"
- ///
- public static LocalisableString ClickToReplaceBackground => new TranslatableString(getKey(@"click_to_replace_background"), @"Click to replace the background image");
-
///
/// "Ruleset ({0})"
///
diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs
index 9a194dba47..f055633d64 100644
--- a/osu.Game/Online/Chat/MessageFormatter.cs
+++ b/osu.Game/Online/Chat/MessageFormatter.cs
@@ -85,8 +85,8 @@ namespace osu.Game.Online.Chat
if (escapeChars != null)
displayText = escapeChars.Aggregate(displayText, (current, c) => current.Replace($"\\{c}", c.ToString()));
- // Check for encapsulated links
- if (result.Links.Find(l => (l.Index <= index && l.Index + l.Length >= index + m.Length) || (index <= l.Index && index + m.Length >= l.Index + l.Length)) == null)
+ // Check for overlapping links
+ if (!result.Links.Exists(l => l.Overlaps(index, m.Length)))
{
result.Text = result.Text.Remove(index, m.Length).Insert(index, displayText);
@@ -364,7 +364,9 @@ namespace osu.Game.Online.Chat
Argument = argument;
}
- public bool Overlaps(Link otherLink) => Index < otherLink.Index + otherLink.Length && otherLink.Index < Index + Length;
+ public bool Overlaps(Link otherLink) => Overlaps(otherLink.Index, otherLink.Length);
+
+ public bool Overlaps(int otherIndex, int otherLength) => Index < otherIndex + otherLength && otherIndex < Index + Length;
public int CompareTo(Link? otherLink) => Index > otherLink?.Index ? 1 : -1;
}
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
index 136c9cc8e7..a76f4ae955 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
@@ -421,7 +421,7 @@ namespace osu.Game.Online.Leaderboards
{
List
-
+