1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-20 07:17:31 +08:00

Merge branch 'master' into tournament-ban-count

This commit is contained in:
Dean Herbert 2023-12-06 12:05:17 +09:00 committed by GitHub
commit 2605aafe24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 666 additions and 344 deletions

View File

@ -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

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1127.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1201.1" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -5,4 +5,12 @@
<!-- for editor usage -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
</manifest>
<!--
READ_MEDIA_* permissions are available only on API 33 or greater. Devices with older android versions
don't understand the new permissions, so request the old READ_EXTERNAL_STORAGE permission to get storage access.
Since the old permission has no effect on >= API 33, don't request it.
Care needs to be taken to ensure runtime permission checks target the correct permission for the API level.
-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
</manifest>

View File

@ -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);
}
}
}
}

View File

@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}
}

View File

@ -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;

View File

@ -22,13 +22,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <summary>
/// A pattern generator for IHasDistance hit objects.
/// </summary>
internal class DistanceObjectPatternGenerator : PatternGenerator
internal class PathObjectPatternGenerator : PatternGenerator
{
/// <summary>
/// Base osu! slider scoring distance.
/// </summary>
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;
}

View File

@ -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<HitObject> EnumerateTopLevelHitObjects() => Beatmap.HitObjects;
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject) => hitObject.NestedHitObjects;

View File

@ -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<IList<HitSampleInfo>> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List<IList<HitSampleInfo>>(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);

View File

@ -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<Beatmap>(stream);
var working = new TestWorkingBeatmap(decoder.Decode(stream));
IBeatmap beatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo, Array.Empty<Mod>());
Assert.That(beatmap.HitObjects[0].GetEndTime(), Is.EqualTo(3153));
}
}
}
}

View File

@ -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

View File

@ -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<MultiplayerReadyButton>();
// Start match
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
// Abort
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once));
}
private void verifyGameplayStartFlow()
{
checkLocalUserState(MultiplayerUserState.Ready);

View File

@ -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<SkinBlueprint>().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<EditorSidebar>().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<SkinEditorSceneLibrary.SceneButton>().First(b => b.Text.ToString() == "Gameplay"));

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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- <Version 0>]", 1, true, expectedActions: LinkAction.OpenBeatmapSet);
addMessageWithChecks("is now playing [https://dev.ppy.sh/b/252238 IMAGE -MATERIAL- <Version 0>]", 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- <Version 0>]", true, false, LinkAction.OpenBeatmapSet)]
[TestCase("is now playing [https://dev.ppy.sh/b/252238 IMAGE -MATERIAL- <Version 0>]", 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<OsuSpriteText>().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++)
{

View File

@ -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 },
}
};
}
}
}

View File

@ -1,8 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<string> GetSearchableTerms(this IBeatmapInfo beatmapInfo)
public static bool Match(this IBeatmapInfo beatmapInfo, params FilterCriteria.OptionalTextFilter[] filters)
{
var termsList = new List<string>(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}]";

View File

@ -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;
/// <summary>
/// An array of all searchable terms provided in contained metadata.
/// </summary>
@ -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<string> termsList)
{

View File

@ -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;

View File

@ -179,21 +179,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString ClickToSelectTrack => new TranslatableString(getKey(@"click_to_select_track"), @"Click to select a track");
/// <summary>
/// "Click to replace the track"
/// </summary>
public static LocalisableString ClickToReplaceTrack => new TranslatableString(getKey(@"click_to_replace_track"), @"Click to replace the track");
/// <summary>
/// "Click to select a background image"
/// </summary>
public static LocalisableString ClickToSelectBackground => new TranslatableString(getKey(@"click_to_select_background"), @"Click to select a background image");
/// <summary>
/// "Click to replace the background image"
/// </summary>
public static LocalisableString ClickToReplaceBackground => new TranslatableString(getKey(@"click_to_replace_background"), @"Click to replace the background image");
/// <summary>
/// "Ruleset ({0})"
/// </summary>

View File

@ -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;
}

View File

@ -421,7 +421,7 @@ namespace osu.Game.Online.Leaderboards
{
List<MenuItem> items = new List<MenuItem>();
if (Score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null)
if (Score.Mods.Length > 0 && songSelect != null)
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods));
if (Score.Files.Count > 0)

View File

@ -0,0 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Online.Multiplayer
{
public enum GameplayAbortReason
{
LoadTookTooLong,
HostAbortedTheMatch
}
}

View File

@ -107,17 +107,18 @@ namespace osu.Game.Online.Multiplayer
/// </summary>
Task LoadRequested();
/// <summary>
/// Signals that loading of gameplay is to be aborted.
/// </summary>
Task LoadAborted();
/// <summary>
/// Signals that gameplay has started.
/// All users in the <see cref="MultiplayerUserState.Loaded"/> or <see cref="MultiplayerUserState.ReadyForGameplay"/> states should begin gameplay as soon as possible.
/// </summary>
Task GameplayStarted();
/// <summary>
/// Signals that gameplay has been aborted.
/// </summary>
/// <param name="reason">The reason why gameplay was aborted.</param>
Task GameplayAborted(GameplayAbortReason reason);
/// <summary>
/// Signals that the match has ended, all players have finished and results are ready to be displayed.
/// </summary>

View File

@ -77,6 +77,11 @@ namespace osu.Game.Online.Multiplayer
/// <exception cref="InvalidStateException">If an attempt to start the game occurs when the game's (or users') state disallows it.</exception>
Task StartMatch();
/// <summary>
/// As the host of a room, aborts an on-going match.
/// </summary>
Task AbortMatch();
/// <summary>
/// Aborts an ongoing gameplay load.
/// </summary>

View File

@ -73,9 +73,9 @@ namespace osu.Game.Online.Multiplayer
public virtual event Action? LoadRequested;
/// <summary>
/// Invoked when the multiplayer server requests loading of play to be aborted.
/// Invoked when the multiplayer server requests gameplay to be aborted.
/// </summary>
public event Action? LoadAborted;
public event Action<GameplayAbortReason>? GameplayAborted;
/// <summary>
/// Invoked when the multiplayer server requests gameplay to be started.
@ -374,6 +374,8 @@ namespace osu.Game.Online.Multiplayer
public abstract Task AbortGameplay();
public abstract Task AbortMatch();
public abstract Task AddPlaylistItem(MultiplayerPlaylistItem item);
public abstract Task EditPlaylistItem(MultiplayerPlaylistItem item);
@ -682,14 +684,14 @@ namespace osu.Game.Online.Multiplayer
return Task.CompletedTask;
}
Task IMultiplayerClient.LoadAborted()
Task IMultiplayerClient.GameplayAborted(GameplayAbortReason reason)
{
Scheduler.Add(() =>
{
if (Room == null)
return;
LoadAborted?.Invoke();
GameplayAborted?.Invoke(reason);
}, false);
return Task.CompletedTask;

View File

@ -58,7 +58,7 @@ namespace osu.Game.Online.Multiplayer
connection.On<int, MultiplayerUserState>(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);
connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested);
connection.On(nameof(IMultiplayerClient.GameplayStarted), ((IMultiplayerClient)this).GameplayStarted);
connection.On(nameof(IMultiplayerClient.LoadAborted), ((IMultiplayerClient)this).LoadAborted);
connection.On<GameplayAbortReason>(nameof(IMultiplayerClient.GameplayAborted), ((IMultiplayerClient)this).GameplayAborted);
connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
connection.On<int, IEnumerable<APIMod>>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged);
connection.On<int, BeatmapAvailability>(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged);
@ -226,6 +226,16 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.AbortGameplay));
}
public override Task AbortMatch()
{
if (!IsConnected.Value)
return Task.CompletedTask;
Debug.Assert(connection != null);
return connection.InvokeAsync(nameof(IMultiplayerServer.AbortMatch));
}
public override Task AddPlaylistItem(MultiplayerPlaylistItem item)
{
if (!IsConnected.Value)

View File

@ -183,9 +183,7 @@ namespace osu.Game.Overlays
// new results may contain beatmaps from a previous page,
// this is dodgy but matches web behaviour for now.
// see: https://github.com/ppy/osu-web/issues/9270
// todo: replace custom equality compraer with ExceptBy in net6.0
// newCards = newCards.ExceptBy(foundContent.Select(c => c.BeatmapSet.OnlineID), c => c.BeatmapSet.OnlineID);
newCards = newCards.Except(foundContent, BeatmapCardEqualityComparer.Default);
newCards = newCards.ExceptBy(foundContent.Select(c => c.BeatmapSet.OnlineID), c => c.BeatmapSet.OnlineID);
panelLoadTask = LoadComponentsAsync(newCards, loaded =>
{
@ -378,21 +376,5 @@ namespace osu.Game.Overlays
if (shouldShowMore)
filterControl.FetchNextPage();
}
private class BeatmapCardEqualityComparer : IEqualityComparer<BeatmapCard>
{
public static BeatmapCardEqualityComparer Default { get; } = new BeatmapCardEqualityComparer();
public bool Equals(BeatmapCard x, BeatmapCard y)
{
if (ReferenceEquals(x, y)) return true;
if (ReferenceEquals(x, null)) return false;
if (ReferenceEquals(y, null)) return false;
return x.BeatmapSet.Equals(y.BeatmapSet);
}
public int GetHashCode(BeatmapCard obj) => obj.BeatmapSet.GetHashCode();
}
}
}

View File

@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Chat
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = HexaconsIcons.Social,
Icon = HexaconsIcons.Messaging,
Size = new Vector2(24),
},
// Placeholder text

View File

@ -23,6 +23,11 @@ namespace osu.Game.Overlays.Dialog
/// </summary>
protected Action? DangerousAction { get; set; }
/// <summary>
/// The action to perform if cancelled.
/// </summary>
protected Action? CancelAction { get; set; }
protected DangerousActionDialog()
{
HeaderText = DeleteConfirmationDialogStrings.HeaderText;
@ -38,7 +43,8 @@ namespace osu.Game.Overlays.Dialog
},
new PopupDialogCancelButton
{
Text = DeleteConfirmationDialogStrings.Cancel
Text = DeleteConfirmationDialogStrings.Cancel,
Action = () => CancelAction?.Invoke()
}
};
}

View File

@ -17,7 +17,6 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens;
@ -26,6 +25,7 @@ using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.Select;
using osu.Game.Users;
using osu.Game.Utils;
using osuTK;
@ -55,9 +55,6 @@ namespace osu.Game.Overlays.SkinEditor
[Resolved]
private MusicController music { get; set; } = null!;
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
[Resolved]
private Bindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
@ -139,6 +136,12 @@ namespace osu.Game.Overlays.SkinEditor
{
performer?.PerformFromScreen(screen =>
{
if (beatmap.Value is DummyWorkingBeatmap)
{
// presume we don't have anything good to play and just bail.
return;
}
// If we're playing the intro, switch away to another beatmap.
if (beatmap.Value.BeatmapSetInfo.Protected)
{
@ -150,7 +153,7 @@ namespace osu.Game.Overlays.SkinEditor
if (screen is Player)
return;
var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod();
var replayGeneratingMod = beatmap.Value.BeatmapInfo.Ruleset.CreateInstance().GetAutoplayMod();
IReadOnlyList<Mod> usableMods = mods.Value;
@ -285,6 +288,8 @@ namespace osu.Game.Overlays.SkinEditor
private partial class EndlessPlayer : ReplayPlayer
{
protected override UserActivity? InitialActivity => null;
public EndlessPlayer(Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore)
: base(createScore, new PlayerConfiguration
{
@ -294,10 +299,21 @@ namespace osu.Game.Overlays.SkinEditor
{
}
protected override void LoadComplete()
{
base.LoadComplete();
if (!LoadedBeatmapSuccessfully)
Scheduler.AddDelayed(this.Exit, 3000);
}
protected override void Update()
{
base.Update();
if (!LoadedBeatmapSuccessfully)
return;
if (GameplayState.HasPassed)
GameplayClockContainer.Seek(0);
}

View File

@ -446,7 +446,7 @@ namespace osu.Game.Rulesets.Scoring
/// <summary>
/// Given an accuracy (0..1), return the correct <see cref="ScoreRank"/>.
/// </summary>
public static ScoreRank RankFromAccuracy(double accuracy)
public virtual ScoreRank RankFromAccuracy(double accuracy)
{
if (accuracy == accuracy_cutoff_x)
return ScoreRank.X;
@ -466,7 +466,7 @@ namespace osu.Game.Rulesets.Scoring
/// Given a <see cref="ScoreRank"/>, return the cutoff accuracy (0..1).
/// Accuracy must be greater than or equal to the cutoff to qualify for the provided rank.
/// </summary>
public static double AccuracyCutoffFromRank(ScoreRank rank)
public virtual double AccuracyCutoffFromRank(ScoreRank rank)
{
switch (rank)
{

View File

@ -342,23 +342,7 @@ namespace osu.Game.Scoring
switch (r.result)
{
case HitResult.SmallTickHit:
{
int total = value + Statistics.GetValueOrDefault(HitResult.SmallTickMiss);
if (total > 0)
yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName);
break;
}
case HitResult.LargeTickHit:
{
int total = value + Statistics.GetValueOrDefault(HitResult.LargeTickMiss);
if (total > 0)
yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName);
break;
}
case HitResult.LargeBonus:
case HitResult.SmallBonus:
if (MaximumStatistics.TryGetValue(r.result, out int count) && count > 0)

View File

@ -146,13 +146,8 @@ namespace osu.Game.Screens.Edit.Setup
private void updatePlaceholderText()
{
audioTrackChooser.Text = audioTrackChooser.Current.Value == null
? EditorSetupStrings.ClickToSelectTrack
: EditorSetupStrings.ClickToReplaceTrack;
backgroundChooser.Text = backgroundChooser.Current.Value == null
? EditorSetupStrings.ClickToSelectBackground
: EditorSetupStrings.ClickToReplaceBackground;
audioTrackChooser.Text = audioTrackChooser.Current.Value?.Name ?? EditorSetupStrings.ClickToSelectTrack;
backgroundChooser.Text = backgroundChooser.Current.Value?.Name ?? EditorSetupStrings.ClickToSelectBackground;
}
}
}

View File

@ -16,6 +16,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
@ -28,6 +30,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
[CanBeNull]
private IDisposable clickOperation;
[Resolved(canBeNull: true)]
private IDialogOverlay dialogOverlay { get; set; }
private Sample sampleReady;
private Sample sampleReadyAll;
private Sample sampleUnready;
@ -56,7 +61,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
Action = onReadyClick,
Action = onReadyButtonClick,
},
countdownButton = new MultiplayerCountdownButton
{
@ -101,7 +106,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
endOperation();
}
private void onReadyClick()
private void onReadyButtonClick()
{
if (Room == null)
return;
@ -109,9 +114,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
if (isReady() && Client.IsHost && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
startMatch();
else
if (Client.IsHost)
{
if (Room.State == MultiplayerRoomState.Open)
{
if (isReady() && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
startMatch();
else
toggleReady();
}
else
{
if (dialogOverlay == null)
abortMatch();
else
dialogOverlay.Push(new ConfirmAbortDialog(abortMatch, endOperation));
}
}
else if (Room.State != MultiplayerRoomState.Closed)
toggleReady();
bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating;
@ -128,6 +148,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
// gameplay was not started due to an exception; unblock button.
endOperation();
});
void abortMatch() => Client.AbortMatch().FireAndForget(endOperation, _ => endOperation());
}
private void startCountdown(TimeSpan duration)
@ -189,7 +211,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}
readyButton.Enabled.Value = countdownButton.Enabled.Value =
Room.State == MultiplayerRoomState.Open
Room.State != MultiplayerRoomState.Closed
&& CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId
&& !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired
&& !operationInProgress.Value;
@ -198,6 +220,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
if (localUser?.State == MultiplayerUserState.Spectating)
readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0 && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown);
// When the local user is not the host, the button should only be enabled when no match is in progress.
if (!Client.IsHost)
readyButton.Enabled.Value &= Room.State == MultiplayerRoomState.Open;
// At all times, the countdown button should only be enabled when no match is in progress.
countdownButton.Enabled.Value &= Room.State == MultiplayerRoomState.Open;
if (newCountReady == countReady)
return;
@ -219,5 +248,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
countReady = newCountReady;
});
}
public partial class ConfirmAbortDialog : DangerousActionDialog
{
public ConfirmAbortDialog(Action abortMatch, Action cancel)
{
HeaderText = "Are you sure you want to abort the match?";
DangerousAction = abortMatch;
CancelAction = cancel;
}
}
}
}

View File

@ -149,16 +149,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
switch (localUser?.State)
{
default:
Text = "Ready";
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
Text = room.Host?.Equals(localUser) == true
Text = multiplayerClient.IsHost
? $"Start match {countText}"
: $"Waiting for host... {countText}";
break;
default:
// Show the abort button for the host as long as gameplay is in progress.
if (multiplayerClient.IsHost && room.State != MultiplayerRoomState.Open)
Text = "Abort the match";
else
Text = "Ready";
break;
}
}
@ -193,12 +196,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
switch (localUser?.State)
{
default:
setGreen();
// Show the abort button for the host as long as gameplay is in progress.
if (multiplayerClient.IsHost && room.State != MultiplayerRoomState.Open)
setRed();
else
setGreen();
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
if (room?.Host?.Equals(localUser) == true && !room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
if (multiplayerClient.IsHost && !room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
setGreen();
else
setYellow();
@ -206,15 +213,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
break;
}
void setYellow()
{
BackgroundColour = colours.YellowDark;
}
void setYellow() => BackgroundColour = colours.YellowDark;
void setGreen()
{
BackgroundColour = colours.Green;
}
void setGreen() => BackgroundColour = colours.Green;
void setRed() => BackgroundColour = colours.Red;
}
protected override void Dispose(bool isDisposing)

View File

@ -23,7 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
base.LoadComplete();
client.RoomUpdated += onRoomUpdated;
client.LoadAborted += onLoadAborted;
client.GameplayAborted += onGameplayAborted;
onRoomUpdated();
}
@ -39,12 +39,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
transitionFromResults();
}
private void onLoadAborted()
private void onGameplayAborted(GameplayAbortReason reason)
{
// If the server aborts gameplay for this user (due to loading too slow), exit gameplay screens.
if (!this.IsCurrentScreen())
{
Logger.Log("Gameplay aborted because loading the beatmap took too long.", LoggingTarget.Runtime, LogLevel.Important);
switch (reason)
{
case GameplayAbortReason.LoadTookTooLong:
Logger.Log("Gameplay aborted because loading the beatmap took too long.", LoggingTarget.Runtime, LogLevel.Important);
break;
case GameplayAbortReason.HostAbortedTheMatch:
Logger.Log("The host aborted the match.", LoggingTarget.Runtime, LogLevel.Important);
break;
}
this.MakeCurrent();
}
}

View File

@ -29,13 +29,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
/// </summary>
public partial class AccuracyCircle : CompositeDrawable
{
private static readonly double accuracy_x = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.X);
private static readonly double accuracy_s = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.S);
private static readonly double accuracy_a = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.A);
private static readonly double accuracy_b = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.B);
private static readonly double accuracy_c = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.C);
private static readonly double accuracy_d = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.D);
/// <summary>
/// Duration for the transforms causing this component to appear.
/// </summary>
@ -110,12 +103,26 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
private double lastTickPlaybackTime;
private bool isTicking;
private readonly double accuracyX;
private readonly double accuracyS;
private readonly double accuracyA;
private readonly double accuracyB;
private readonly double accuracyC;
private readonly double accuracyD;
private readonly bool withFlair;
public AccuracyCircle(ScoreInfo score, bool withFlair = false)
{
this.score = score;
this.withFlair = withFlair;
ScoreProcessor scoreProcessor = score.Ruleset.CreateInstance().CreateScoreProcessor();
accuracyX = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.X);
accuracyS = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.S);
accuracyA = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.A);
accuracyB = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.B);
accuracyC = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.C);
accuracyD = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.D);
}
[BackgroundDependencyLoader]
@ -158,49 +165,49 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.X),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = accuracy_x }
Current = { Value = accuracyX }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.S),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = accuracy_x - virtual_ss_percentage }
Current = { Value = accuracyX - virtual_ss_percentage }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.A),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = accuracy_s }
Current = { Value = accuracyS }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.B),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = accuracy_a }
Current = { Value = accuracyA }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.C),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = accuracy_b }
Current = { Value = accuracyB }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.D),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = accuracy_c }
Current = { Value = accuracyC }
},
new RankNotch((float)accuracy_x),
new RankNotch((float)(accuracy_x - virtual_ss_percentage)),
new RankNotch((float)accuracy_s),
new RankNotch((float)accuracy_a),
new RankNotch((float)accuracy_b),
new RankNotch((float)accuracy_c),
new RankNotch((float)accuracyX),
new RankNotch((float)(accuracyX - virtual_ss_percentage)),
new RankNotch((float)accuracyS),
new RankNotch((float)accuracyA),
new RankNotch((float)accuracyB),
new RankNotch((float)accuracyC),
new BufferedContainer
{
Name = "Graded circle mask",
@ -228,13 +235,13 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
Padding = new MarginPadding { Vertical = -15, Horizontal = -20 },
Children = new[]
{
new RankBadge(accuracyD, Interpolation.Lerp(accuracyD, accuracyC, 0.5), getRank(ScoreRank.D)),
new RankBadge(accuracyC, Interpolation.Lerp(accuracyC, accuracyB, 0.5), getRank(ScoreRank.C)),
new RankBadge(accuracyB, Interpolation.Lerp(accuracyB, accuracyA, 0.5), getRank(ScoreRank.B)),
// The S and A badges are moved down slightly to prevent collision with the SS badge.
new RankBadge(accuracy_x, accuracy_x, getRank(ScoreRank.X)),
new RankBadge(accuracy_s, Interpolation.Lerp(accuracy_s, (accuracy_x - virtual_ss_percentage), 0.25), getRank(ScoreRank.S)),
new RankBadge(accuracy_a, Interpolation.Lerp(accuracy_a, accuracy_s, 0.25), getRank(ScoreRank.A)),
new RankBadge(accuracy_b, Interpolation.Lerp(accuracy_b, accuracy_a, 0.5), getRank(ScoreRank.B)),
new RankBadge(accuracy_c, Interpolation.Lerp(accuracy_c, accuracy_b, 0.5), getRank(ScoreRank.C)),
new RankBadge(accuracy_d, Interpolation.Lerp(accuracy_d, accuracy_c, 0.5), getRank(ScoreRank.D)),
new RankBadge(accuracyA, Interpolation.Lerp(accuracyA, accuracyS, 0.25), getRank(ScoreRank.A)),
new RankBadge(accuracyS, Interpolation.Lerp(accuracyS, (accuracyX - virtual_ss_percentage), 0.25), getRank(ScoreRank.S)),
new RankBadge(accuracyX, accuracyX, getRank(ScoreRank.X)),
}
},
rankText = new RankText(score.Rank)
@ -280,10 +287,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
double targetAccuracy = score.Accuracy;
double[] notchPercentages =
{
accuracy_s,
accuracy_a,
accuracy_b,
accuracy_c,
accuracyS,
accuracyA,
accuracyB,
accuracyC,
};
// Ensure the gauge overshoots or undershoots a bit so it doesn't land in the gaps of the inner graded circle (caused by `RankNotch`es),
@ -302,7 +309,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
if (score.Rank == ScoreRank.X || score.Rank == ScoreRank.XH)
targetAccuracy = 1;
else
targetAccuracy = Math.Min(accuracy_x - virtual_ss_percentage - NOTCH_WIDTH_PERCENTAGE / 2, targetAccuracy);
targetAccuracy = Math.Min(accuracyX - virtual_ss_percentage - NOTCH_WIDTH_PERCENTAGE / 2, targetAccuracy);
// The accuracy circle gauge visually fills up a bit too much.
// This wouldn't normally matter but we want it to align properly with the inner graded circle in the above cases.
@ -339,7 +346,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
if (badge.Accuracy > score.Accuracy)
continue;
using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(accuracy_x - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION))
using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(accuracyX - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION))
{
badge.Appear();

View File

@ -41,6 +41,21 @@ namespace osu.Game.Screens.Select.Carousel
return match;
}
if (criteria.SearchTerms.Length > 0)
{
match = BeatmapInfo.Match(criteria.SearchTerms);
// if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs.
// this should be done after text matching so we can prioritise matching numbers in metadata.
if (!match && criteria.SearchNumber.HasValue)
{
match = (BeatmapInfo.OnlineID == criteria.SearchNumber.Value) ||
(BeatmapInfo.BeatmapSet?.OnlineID == criteria.SearchNumber.Value);
}
}
if (!match) return false;
match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarRating);
match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(BeatmapInfo.Difficulty.ApproachRate);
match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(BeatmapInfo.Difficulty.DrainRate);
@ -64,40 +79,6 @@ namespace osu.Game.Screens.Select.Carousel
if (!match) return false;
if (criteria.SearchTerms.Length > 0)
{
var searchableTerms = BeatmapInfo.GetSearchableTerms();
foreach (FilterCriteria.OptionalTextFilter criteriaTerm in criteria.SearchTerms)
{
bool any = false;
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
foreach (string searchTerm in searchableTerms)
{
if (!criteriaTerm.Matches(searchTerm)) continue;
any = true;
break;
}
if (any) continue;
match = false;
break;
}
// if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs.
// this should be done after text matching so we can prioritise matching numbers in metadata.
if (!match && criteria.SearchNumber.HasValue)
{
match = (BeatmapInfo.OnlineID == criteria.SearchNumber.Value) ||
(BeatmapInfo.BeatmapSet?.OnlineID == criteria.SearchNumber.Value);
}
}
if (!match) return false;
match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true;
if (match && criteria.RulesetCriteria != null)
match &= criteria.RulesetCriteria.Matches(BeatmapInfo);

View File

@ -86,16 +86,20 @@ namespace osu.Game.Screens.Select.Carousel
items.ForEach(c => c.Filter(criteria));
criteriaComparer = Comparer<CarouselItem>.Create((x, y) =>
// Sorting is expensive, so only perform if it's actually changed.
if (lastCriteria?.Sort != criteria.Sort)
{
int comparison = x.CompareTo(criteria, y);
if (comparison != 0)
return comparison;
criteriaComparer = Comparer<CarouselItem>.Create((x, y) =>
{
int comparison = x.CompareTo(criteria, y);
if (comparison != 0)
return comparison;
return x.ItemID.CompareTo(y.ItemID);
});
return x.ItemID.CompareTo(y.ItemID);
});
items.Sort(criteriaComparer);
items.Sort(criteriaComparer);
}
lastCriteria = criteria;
}

View File

@ -176,13 +176,15 @@ namespace osu.Game.Screens.Select
{
default:
case MatchMode.Substring:
return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase);
// Note that we are using ordinal here to avoid performance issues caused by globalisation concerns.
// See https://github.com/ppy/osu/issues/11571 / https://github.com/dotnet/docs/issues/18423.
return value.Contains(SearchTerm, StringComparison.OrdinalIgnoreCase);
case MatchMode.IsolatedPhrase:
return Regex.IsMatch(value, $@"(^|\s){Regex.Escape(searchTerm)}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
case MatchMode.FullPhrase:
return CultureInfo.InvariantCulture.CompareInfo.Compare(value, searchTerm, CompareOptions.IgnoreCase) == 0;
return CultureInfo.InvariantCulture.CompareInfo.Compare(value, searchTerm, CompareOptions.OrdinalIgnoreCase) == 0;
}
}

View File

@ -396,6 +396,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
return Task.CompletedTask;
}
public override async Task AbortMatch()
{
ChangeUserState(api.LocalUser.Value.Id, MultiplayerUserState.Idle);
await ((IMultiplayerClient)this).GameplayAborted(GameplayAbortReason.HostAbortedTheMatch).ConfigureAwait(false);
}
public async Task AddUserPlaylistItem(int userId, MultiplayerPlaylistItem item)
{
Debug.Assert(ServerRoom != null);

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.1127.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.1201.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1127.0" />
<PackageReference Include="Sentry" Version="3.40.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->

View File

@ -23,6 +23,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.1127.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.1201.1" />
</ItemGroup>
</Project>