1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-20 14:00:21 +08:00

Compare commits

...

174 Commits

164 changed files with 3232 additions and 1092 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1008.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1028.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
+1 -1
View File
@@ -189,7 +189,7 @@ namespace osu.Desktop
}
// user party
if (!hideIdentifiableInformation && multiplayerClient.Room != null)
if (!hideIdentifiableInformation && multiplayerClient.Room != null && multiplayerClient.Room.Settings.MatchType != MatchType.Matchmaking)
{
MultiplayerRoom room = multiplayerClient.Room;
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
private float halfCatcherWidth;
public override int Version => 20250306;
public override int Version => 20251020;
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
@@ -51,7 +51,8 @@ namespace osu.Game.Rulesets.Catch.Mods
return string.Empty;
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
string format(string acronym, DifficultyBindable bindable)
=> $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
}
}
+17 -4
View File
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
@@ -53,13 +54,25 @@ namespace osu.Game.Rulesets.Catch.Objects
public override IEnumerable<string> LookupNames => lookup_names;
public BananaHitSampleInfo(int volume = 100)
: base(string.Empty, volume: volume)
public BananaHitSampleInfo()
: this(string.Empty)
{
}
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default)
=> new BananaHitSampleInfo(newVolume.GetOr(Volume));
public BananaHitSampleInfo(HitSampleInfo info)
: this(info.Name, info.Bank, info.Suffix, info.Volume, info.EditorAutoBank, info.UseBeatmapSamples)
{
}
private BananaHitSampleInfo(string name, string bank = SampleControlPoint.DEFAULT_BANK, string? suffix = null, int volume = 100, bool editorAutoBank = true, bool useBeatmapSamples = false)
: base(name, bank, suffix, volume, editorAutoBank, useBeatmapSamples)
{
}
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default,
Optional<bool> newEditorAutoBank = default, Optional<bool> newUseBeatmapSamples = default)
=> new BananaHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume),
newEditorAutoBank.GetOr(EditorAutoBank), newUseBeatmapSamples.GetOr(UseBeatmapSamples));
public bool Equals(BananaHitSampleInfo? other)
=> other != null;
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
StartTime = time,
BananaIndex = count,
Samples = new List<HitSampleInfo> { new Banana.BananaHitSampleInfo(CreateHitSampleInfo().Volume) }
Samples = new List<HitSampleInfo> { new Banana.BananaHitSampleInfo(CreateHitSampleInfo()) }
});
count++;
@@ -7,5 +7,15 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModDifficultyAdjust : ModDifficultyAdjust
{
public override DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 10,
// Use larger extended limits for mania to include OD values that occur with EZ or HR enabled
ExtendedMaxValue = 15,
ExtendedMinValue = -15,
ReadCurrentFromDifficulty = diff => diff.OverallDifficulty,
};
}
}
@@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods;
using osu.Framework.Graphics.Sprites;
using System.Collections.Generic;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
@@ -34,6 +35,8 @@ namespace osu.Game.Rulesets.Mania.Mods
{
var maniaBeatmap = (ManiaBeatmap)beatmap;
double mostCommonBeatLengthBefore = beatmap.GetMostCommonBeatLength();
var newObjects = new List<ManiaHitObject>();
foreach (var h in beatmap.HitObjects.OfType<HoldNote>())
@@ -48,6 +51,17 @@ namespace osu.Game.Rulesets.Mania.Mods
}
maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType<Note>().Concat(newObjects).OrderBy(h => h.StartTime).ToList();
double mostCommonBeatLengthAfter = beatmap.GetMostCommonBeatLength();
// the process of removing hold notes can result in shortening the beatmap's play time,
// and therefore, as a side effect, changing the most common BPM, which will change scroll speed.
// to compensate for this, apply a multiplier to effect points in order to maintain the beatmap's original intended scroll speed.
if (!Precision.AlmostEquals(mostCommonBeatLengthBefore, mostCommonBeatLengthAfter))
{
foreach (var effectPoint in beatmap.ControlPointInfo.EffectPoints)
effectPoint.ScrollSpeed *= mostCommonBeatLengthBefore / mostCommonBeatLengthAfter;
}
}
}
}
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
private const double star_rating_multiplier = 0.0265;
public override int Version => 20250306;
public override int Version => 20251020;
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
@@ -51,7 +51,8 @@ namespace osu.Game.Rulesets.Osu.Mods
return string.Empty;
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
string format(string acronym, DifficultyBindable bindable)
=> $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
}
}
@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Configuration;
using osu.Game.Localisation;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Screens.Play.PlayerSettings;
@@ -13,19 +14,19 @@ namespace osu.Game.Rulesets.Osu.UI
{
private readonly OsuRulesetConfigManager config;
[SettingSource("Show click markers", SettingControlType = typeof(PlayerCheckbox))]
[SettingSource(typeof(PlayerSettingsOverlayStrings), nameof(PlayerSettingsOverlayStrings.ShowClickMarkers), SettingControlType = typeof(PlayerCheckbox))]
public BindableBool ShowClickMarkers { get; } = new BindableBool();
[SettingSource("Show frame markers", SettingControlType = typeof(PlayerCheckbox))]
[SettingSource(typeof(PlayerSettingsOverlayStrings), nameof(PlayerSettingsOverlayStrings.ShowFrameMarkers), SettingControlType = typeof(PlayerCheckbox))]
public BindableBool ShowAimMarkers { get; } = new BindableBool();
[SettingSource("Show cursor path", SettingControlType = typeof(PlayerCheckbox))]
[SettingSource(typeof(PlayerSettingsOverlayStrings), nameof(PlayerSettingsOverlayStrings.ShowCursorPath), SettingControlType = typeof(PlayerCheckbox))]
public BindableBool ShowCursorPath { get; } = new BindableBool();
[SettingSource("Hide gameplay cursor", SettingControlType = typeof(PlayerCheckbox))]
[SettingSource(typeof(PlayerSettingsOverlayStrings), nameof(PlayerSettingsOverlayStrings.HideGameplayCursor), SettingControlType = typeof(PlayerCheckbox))]
public BindableBool HideSkinCursor { get; } = new BindableBool();
[SettingSource("Display length", SettingControlType = typeof(PlayerSliderBar<int>))]
[SettingSource(typeof(PlayerSettingsOverlayStrings), nameof(PlayerSettingsOverlayStrings.DisplayLength), SettingControlType = typeof(PlayerSliderBar<int>))]
public BindableInt DisplayLength { get; } = new BindableInt
{
MinValue = 200,
@@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.UI
};
public ReplayAnalysisSettings(OsuRulesetConfigManager config)
: base("Analysis Settings")
: base(PlayerSettingsOverlayStrings.AnalysisSettingsTitle)
{
this.config = config;
}
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private bool isRelax;
private bool isConvert;
public override int Version => 20250306;
public override int Version => 20251020;
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
@@ -34,7 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
return string.Empty;
string format(string acronym, DifficultyBindable bindable, int digits) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(digits)}";
string format(string acronym, DifficultyBindable bindable, int digits)
=> $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(digits)}";
}
}
@@ -13,7 +13,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
public const int SAMPLE_VOLUME_THRESHOLD_MEDIUM = 60;
public VolumeAwareHitSampleInfo(HitSampleInfo sampleInfo, bool isStrong = false)
: base(sampleInfo.Name, isStrong ? BANK_STRONG : getBank(sampleInfo.Bank, sampleInfo.Name, sampleInfo.Volume), sampleInfo.Suffix, sampleInfo.Volume)
: base(sampleInfo.Name, isStrong ? BANK_STRONG : getBank(sampleInfo.Bank, sampleInfo.Name, sampleInfo.Volume), sampleInfo.Suffix, sampleInfo.Volume,
sampleInfo.EditorAutoBank, sampleInfo.UseBeatmapSamples)
{
}
@@ -240,7 +240,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
private class LegacyTaikoSampleInfo : HitSampleInfo
{
public LegacyTaikoSampleInfo(HitSampleInfo sampleInfo)
: base(sampleInfo.Name, sampleInfo.Bank, sampleInfo.Suffix, sampleInfo.Volume)
: base(sampleInfo.Name, sampleInfo.Bank, sampleInfo.Suffix, sampleInfo.Volume, sampleInfo.EditorAutoBank, sampleInfo.UseBeatmapSamples)
{
}
@@ -11,6 +11,7 @@ using NUnit.Framework;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
@@ -236,6 +237,31 @@ namespace osu.Game.Tests.Beatmaps.Formats
Is.EquivalentTo(originalSlider.Path.ControlPoints.Select(p => p.Position)));
}
[Test]
public void TestEncodeCustomSampleBanks()
{
var beatmap = new Beatmap
{
HitObjects =
{
new HitCircle { StartTime = 100, Samples = [new HitSampleInfo(HitSampleInfo.HIT_NORMAL)] },
new HitCircle { StartTime = 200, Samples = [new HitSampleInfo(HitSampleInfo.HIT_NORMAL, useBeatmapSamples: true)] },
new HitCircle { StartTime = 300, Samples = [new HitSampleInfo(HitSampleInfo.HIT_NORMAL, suffix: "3", useBeatmapSamples: true)] },
}
};
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy((beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty))), string.Empty);
Assert.That(decodedAfterEncode.beatmap.HitObjects[0].Samples[0].Suffix, Is.Null);
Assert.That(decodedAfterEncode.beatmap.HitObjects[0].Samples[0].UseBeatmapSamples, Is.False);
Assert.That(decodedAfterEncode.beatmap.HitObjects[1].Samples[0].Suffix, Is.Null);
Assert.That(decodedAfterEncode.beatmap.HitObjects[1].Samples[0].UseBeatmapSamples, Is.True);
Assert.That(decodedAfterEncode.beatmap.HitObjects[2].Samples[0].Suffix, Is.EqualTo("3"));
Assert.That(decodedAfterEncode.beatmap.HitObjects[2].Samples[0].UseBeatmapSamples, Is.True);
}
private bool areComboColoursEqual(IHasComboColours a, IHasComboColours b)
{
// equal to null, no need to SequenceEqual
@@ -59,7 +59,15 @@ namespace osu.Game.Tests.Beatmaps.IO
// Ensure importer encoding is correct
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"fractional-coordinates.olz"));
AddAssert("hit object has fractional position", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(383.99997).Within(0.00001));
AddAssert("second slider has fractional position",
() => ((IHasXPosition)beatmap.Beatmap.HitObjects[1]).X,
() => Is.EqualTo(-3.0517578E-05).Within(0.00001));
AddAssert("second slider path has fractional coordinates",
() => ((IHasPath)beatmap.Beatmap.HitObjects[1]).Path.ControlPoints[1].Position.X,
() => Is.EqualTo(191.999939).Within(0.00001));
AddAssert("second hit circle has fractional position",
() => ((IHasYPosition)beatmap.Beatmap.HitObjects[3]).Y,
() => Is.EqualTo(383.99997).Within(0.00001));
// Ensure exporter legacy conversion is correct
AddStep("export", () =>
@@ -71,7 +79,15 @@ namespace osu.Game.Tests.Beatmaps.IO
});
AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
AddAssert("hit object is snapped", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(384).Within(0.00001));
AddAssert("second slider is snapped",
() => ((IHasXPosition)beatmap.Beatmap.HitObjects[1]).X,
() => Is.EqualTo(0).Within(0.00001));
AddAssert("second slider path is snapped",
() => ((IHasPath)beatmap.Beatmap.HitObjects[1]).Path.ControlPoints[1].Position.X,
() => Is.EqualTo(192).Within(0.00001));
AddAssert("second hit circle is snapped",
() => ((IHasYPosition)beatmap.Beatmap.HitObjects[3]).Y,
() => Is.EqualTo(384).Within(0.00001));
}
[Test]
@@ -17,6 +17,7 @@ namespace osu.Game.Tests.Extensions
[TestCase(0, true, 0, ExpectedResult = "0%")]
[TestCase(1, true, 0, ExpectedResult = "1%")]
[TestCase(50, true, 0, ExpectedResult = "50%")]
[SetCulture("")] // invariant culture
public string TestInteger(int input, bool percent, int decimalDigits)
{
return input.ToStandardFormattedString(decimalDigits, percent);
@@ -39,6 +40,7 @@ namespace osu.Game.Tests.Extensions
[TestCase(0.48333, true, 2, ExpectedResult = "48%")]
[TestCase(0.48333, true, 4, ExpectedResult = "48.33%")]
[TestCase(1, true, 0, ExpectedResult = "100%")]
[SetCulture("")] // invariant culture
public string TestDouble(double input, bool percent, int decimalDigits)
{
return input.ToStandardFormattedString(decimalDigits, percent);
@@ -47,9 +49,9 @@ namespace osu.Game.Tests.Extensions
[Test]
[SetCulture("fr-FR")]
[TestCase(0.4, true, 2, ExpectedResult = "40%")]
[TestCase(1e-6, false, 6, ExpectedResult = "0.000001")]
[TestCase(0.48333, true, 4, ExpectedResult = "48.33%")]
public string TestCultureInsensitivity(double input, bool percent, int decimalDigits)
[TestCase(1e-6, false, 6, ExpectedResult = "0,000001")]
[TestCase(0.48333, true, 4, ExpectedResult = "48,33%")]
public string TestCultureSensitivity(double input, bool percent, int decimalDigits)
{
return input.ToStandardFormattedString(decimalDigits, percent);
}
@@ -299,6 +299,23 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
}
[Test]
[TestCase("artist")]
[TestCase("unicode")]
public void TestCriteriaNotMatchingArtist(string excludedTerm)
{
var beatmap = getExampleBeatmap();
var criteria = new FilterCriteria
{
Artist = new FilterCriteria.OptionalTextFilter { SearchTerm = excludedTerm, ExcludeTerm = true }
};
var carouselItem = new CarouselBeatmap(beatmap);
carouselItem.Filter(criteria);
Assert.True(carouselItem.Filtered.Value);
}
[TestCase("simple", false)]
[TestCase("\"style/clean\"", false)]
[TestCase("\"style/clean\"!", false)]
@@ -350,6 +367,41 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual(true, carouselItem.Filtered.Value);
}
[Test]
public void TestCriteriaMatchingTagExcluded()
{
var beatmap = getExampleBeatmap();
var criteria = new FilterCriteria
{
UserTags =
[
new FilterCriteria.OptionalTextFilter { SearchTerm = "\"song representation/simple\"!", ExcludeTerm = true },
]
};
var carouselItem = new CarouselBeatmap(beatmap);
carouselItem.Filter(criteria);
Assert.AreEqual(true, carouselItem.Filtered.Value);
}
[Test]
public void TestCriteriaOneTagIncludedAndOneTagExcluded()
{
var beatmap = getExampleBeatmap();
var criteria = new FilterCriteria
{
UserTags =
[
new FilterCriteria.OptionalTextFilter { SearchTerm = "\"song representation/simple\"!" },
new FilterCriteria.OptionalTextFilter { SearchTerm = "\"style/clean\"!", ExcludeTerm = true }
]
};
var carouselItem = new CarouselBeatmap(beatmap);
carouselItem.Filter(criteria);
Assert.AreEqual(true, carouselItem.Filtered.Value);
}
[Test]
public void TestBeatmapMustHaveAtLeastOneTagIfUserTagFilterActive()
{
@@ -29,17 +29,17 @@ namespace osu.Game.Tests.Online.Matchmaking
new SoloScoreInfo { UserID = 3, TotalScore = 750 },
], placement_points);
Assert.AreEqual(8, state.Users[1].Points);
Assert.AreEqual(1, state.Users[1].Placement);
Assert.AreEqual(1, state.Users[1].Rounds[1].Placement);
Assert.AreEqual(8, state.Users.GetOrAdd(1).Points);
Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(1, state.Users.GetOrAdd(1).Rounds.GetOrAdd(1).Placement);
Assert.AreEqual(6, state.Users[2].Points);
Assert.AreEqual(3, state.Users[2].Placement);
Assert.AreEqual(3, state.Users[2].Rounds[1].Placement);
Assert.AreEqual(6, state.Users.GetOrAdd(2).Points);
Assert.AreEqual(3, state.Users.GetOrAdd(2).Placement);
Assert.AreEqual(3, state.Users.GetOrAdd(2).Rounds.GetOrAdd(1).Placement);
Assert.AreEqual(7, state.Users[3].Points);
Assert.AreEqual(2, state.Users[3].Placement);
Assert.AreEqual(2, state.Users[3].Rounds[1].Placement);
Assert.AreEqual(7, state.Users.GetOrAdd(3).Points);
Assert.AreEqual(2, state.Users.GetOrAdd(3).Placement);
Assert.AreEqual(2, state.Users.GetOrAdd(3).Rounds.GetOrAdd(1).Placement);
// 2 -> 1 -> 3
@@ -51,17 +51,17 @@ namespace osu.Game.Tests.Online.Matchmaking
new SoloScoreInfo { UserID = 3, TotalScore = 500 },
], placement_points);
Assert.AreEqual(15, state.Users[1].Points);
Assert.AreEqual(1, state.Users[1].Placement);
Assert.AreEqual(2, state.Users[1].Rounds[2].Placement);
Assert.AreEqual(15, state.Users.GetOrAdd(1).Points);
Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(2, state.Users.GetOrAdd(1).Rounds.GetOrAdd(2).Placement);
Assert.AreEqual(14, state.Users[2].Points);
Assert.AreEqual(2, state.Users[2].Placement);
Assert.AreEqual(1, state.Users[2].Rounds[2].Placement);
Assert.AreEqual(14, state.Users.GetOrAdd(2).Points);
Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement);
Assert.AreEqual(1, state.Users.GetOrAdd(2).Rounds.GetOrAdd(2).Placement);
Assert.AreEqual(13, state.Users[3].Points);
Assert.AreEqual(3, state.Users[3].Placement);
Assert.AreEqual(3, state.Users[3].Rounds[2].Placement);
Assert.AreEqual(13, state.Users.GetOrAdd(3).Points);
Assert.AreEqual(3, state.Users.GetOrAdd(3).Placement);
Assert.AreEqual(3, state.Users.GetOrAdd(3).Rounds.GetOrAdd(2).Placement);
}
[Test]
@@ -80,21 +80,21 @@ namespace osu.Game.Tests.Online.Matchmaking
new SoloScoreInfo { UserID = 4, TotalScore = 500 },
], placement_points);
Assert.AreEqual(7, state.Users[1].Points);
Assert.AreEqual(1, state.Users[1].Placement);
Assert.AreEqual(2, state.Users[1].Rounds[1].Placement);
Assert.AreEqual(7, state.Users.GetOrAdd(1).Points);
Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(2, state.Users.GetOrAdd(1).Rounds.GetOrAdd(1).Placement);
Assert.AreEqual(7, state.Users[2].Points);
Assert.AreEqual(2, state.Users[2].Placement);
Assert.AreEqual(2, state.Users[2].Rounds[1].Placement);
Assert.AreEqual(7, state.Users.GetOrAdd(2).Points);
Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement);
Assert.AreEqual(2, state.Users.GetOrAdd(2).Rounds.GetOrAdd(1).Placement);
Assert.AreEqual(5, state.Users[3].Points);
Assert.AreEqual(3, state.Users[3].Placement);
Assert.AreEqual(4, state.Users[3].Rounds[1].Placement);
Assert.AreEqual(5, state.Users.GetOrAdd(3).Points);
Assert.AreEqual(3, state.Users.GetOrAdd(3).Placement);
Assert.AreEqual(4, state.Users.GetOrAdd(3).Rounds.GetOrAdd(1).Placement);
Assert.AreEqual(5, state.Users[4].Points);
Assert.AreEqual(4, state.Users[4].Placement);
Assert.AreEqual(4, state.Users[4].Rounds[1].Placement);
Assert.AreEqual(5, state.Users.GetOrAdd(4).Points);
Assert.AreEqual(4, state.Users.GetOrAdd(4).Placement);
Assert.AreEqual(4, state.Users.GetOrAdd(4).Rounds.GetOrAdd(1).Placement);
}
[Test]
@@ -120,8 +120,8 @@ namespace osu.Game.Tests.Online.Matchmaking
new SoloScoreInfo { UserID = 2, TotalScore = 1000 },
], placement_points);
Assert.AreEqual(1, state.Users[1].Placement);
Assert.AreEqual(2, state.Users[2].Placement);
Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement);
}
[Test]
@@ -142,12 +142,12 @@ namespace osu.Game.Tests.Online.Matchmaking
new SoloScoreInfo { UserID = 5, TotalScore = 1000 },
], placement_points);
Assert.AreEqual(1, state.Users[1].Placement);
Assert.AreEqual(2, state.Users[2].Placement);
Assert.AreEqual(3, state.Users[3].Placement);
Assert.AreEqual(4, state.Users[4].Placement);
Assert.AreEqual(5, state.Users[5].Placement);
Assert.AreEqual(6, state.Users[6].Placement);
Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement);
Assert.AreEqual(3, state.Users.GetOrAdd(3).Placement);
Assert.AreEqual(4, state.Users.GetOrAdd(4).Placement);
Assert.AreEqual(5, state.Users.GetOrAdd(5).Placement);
Assert.AreEqual(6, state.Users.GetOrAdd(6).Placement);
}
}
}
@@ -149,6 +149,8 @@ namespace osu.Game.Tests.Rulesets
public IBindable<double> AggregateFrequency => throw new NotImplementedException();
public IBindable<double> AggregateTempo => throw new NotImplementedException();
public void Invalidate(string name) => throw new NotImplementedException();
public int PlaybackConcurrency { get; set; }
public void AddExtension(string extension) => throw new NotImplementedException();
@@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Components
};
for (int i = 1; i <= 100; i++)
((DummyAPIAccess)API).Friends.Add(new APIRelation { TargetID = i, TargetUser = new APIUser { Username = $"Friend {i}" } });
((DummyAPIAccess)API).LocalUserState.Friends.Add(new APIRelation { TargetID = i, TargetUser = new APIUser { Username = $"Friend {i}" } });
});
[Test]
@@ -75,7 +75,9 @@ namespace osu.Game.Tests.Visual.Components
});
AddUntilStep("chat overlay opened", () => chatOverlay.State.Value, () => Is.EqualTo(Visibility.Visible));
AddUntilStep("user channel selected", () => channelManager.CurrentChannel.Value.Name, () => Is.EqualTo(((DummyAPIAccess)API).Friends[0].TargetUser!.Username));
AddUntilStep("user channel selected",
() => channelManager.CurrentChannel.Value.Name,
() => Is.EqualTo(((DummyAPIAccess)API).LocalUserState.Friends[0].TargetUser!.Username));
}
[Test]
@@ -90,8 +90,8 @@ namespace osu.Game.Tests.Visual.Gameplay
var api = (DummyAPIAccess)API;
api.Friends.Clear();
api.Friends.Add(new APIRelation
api.LocalUserState.Friends.Clear();
api.LocalUserState.Friends.Add(new APIRelation
{
Mutual = true,
RelationType = RelationType.Friend,
@@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Gameplay
new ScoreInfo { User = new APIUser { Username = "Top", Id = 2 }, TotalScore = 900_000, Accuracy = 0.99, MaxCombo = 999 },
new ScoreInfo { User = new APIUser { Username = "Second", Id = 14 }, TotalScore = 800_000, Accuracy = 0.9, MaxCombo = 888 },
new ScoreInfo { User = friend, TotalScore = 700_000, Accuracy = 0.88, MaxCombo = 777 },
}, 3, null);
}, scoresRequested: 50, totalScores: 3, null);
});
createLeaderboard();
@@ -129,8 +129,8 @@ namespace osu.Game.Tests.Visual.Gameplay
var api = (DummyAPIAccess)API;
api.Friends.Clear();
api.Friends.Add(new APIRelation
api.LocalUserState.Friends.Clear();
api.LocalUserState.Friends.Add(new APIRelation
{
Mutual = true,
RelationType = RelationType.Friend,
@@ -144,7 +144,7 @@ namespace osu.Game.Tests.Visual.Gameplay
new ScoreInfo { User = new APIUser { Username = "Top", Id = 2 }, TotalScore = 900_000_000, Accuracy = 0.99, MaxCombo = 999999 },
new ScoreInfo { User = new APIUser { Username = "Second", Id = 14 }, TotalScore = 800_000_000, Accuracy = 0.9, MaxCombo = 888888 },
new ScoreInfo { User = friend, TotalScore = 700_000_000, Accuracy = 0.88, MaxCombo = 777777 },
}, 3, null);
}, scoresRequested: 50, totalScores: 3, null);
});
createLeaderboard();
@@ -170,7 +170,7 @@ namespace osu.Game.Tests.Visual.Gameplay
scores.Add(new ScoreInfo { User = new APIUser { Username = $"Player {i + 1}" }, TotalScore = RNG.Next(700_000, 1_000_000) });
// this is dodgy but anything less dodgy is a lot of work
((Bindable<LeaderboardScores?>)leaderboardManager.Scores).Value = LeaderboardScores.Success(scores, scores.Count, null);
((Bindable<LeaderboardScores?>)leaderboardManager.Scores).Value = LeaderboardScores.Success(scores, scoresRequested: 50, scores.Count, null);
gameplayState.ScoreProcessor.TotalScore.Value = 0;
});
@@ -208,7 +208,7 @@ namespace osu.Game.Tests.Visual.Gameplay
new ScoreInfo { User = new APIUser { Username = "smoogipoo", Id = 1040328 }, TotalScore = 800_000, Accuracy = 0.9, MaxCombo = 888 },
new ScoreInfo { User = new APIUser { Username = "flyte", Id = 3103765 }, TotalScore = 700_000, Accuracy = 0.9, MaxCombo = 888 },
new ScoreInfo { User = new APIUser { Username = "frenzibyte", Id = 14210502 }, TotalScore = 600_000, Accuracy = 0.9, MaxCombo = 777 },
}, 4, null);
}, scoresRequested: 50, totalScores: 4, null);
});
createLeaderboard();
@@ -223,7 +223,7 @@ namespace osu.Game.Tests.Visual.Gameplay
((Bindable<LeaderboardScores?>)leaderboardManager.Scores).Value = LeaderboardScores.Success(new[]
{
new ScoreInfo { User = new APIUser { Username = "Quit", Id = 3 }, TotalScore = 100_000, Accuracy = 0.99, MaxCombo = 999 },
}, 1, null);
}, scoresRequested: 50, totalScores: 1, null);
});
createLeaderboard();
@@ -35,7 +35,9 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
protected override Drawable CreateDefaultImplementation() => new ArgonKeyCounterDisplay();
protected override Drawable CreateArgonImplementation() => new ArgonKeyCounterDisplay();
protected override Drawable CreateDefaultImplementation() => new DefaultKeyCounterDisplay();
protected override Drawable CreateLegacyImplementation() => new LegacyKeyCounterDisplay();
}
@@ -37,7 +37,8 @@ namespace osu.Game.Tests.Visual.Gameplay
TotalScore = 10_000 * (100 - i),
Position = i,
}).ToArray(),
1337,
scoresRequested: 100,
totalScores: 100,
null
);
});
@@ -84,7 +85,8 @@ namespace osu.Game.Tests.Visual.Gameplay
TotalScore = 600_000 + 10_000 * (40 - i),
Position = i,
}).ToArray(),
1337,
scoresRequested: 50,
totalScores: 40,
null
);
});
@@ -131,7 +133,8 @@ namespace osu.Game.Tests.Visual.Gameplay
TotalScore = 500_000 + 10_000 * (50 - i),
Position = i
}).ToArray(),
1337,
scoresRequested: 50,
totalScores: 1337,
new ScoreInfo { TotalScore = 200_000 }
);
});
@@ -1,10 +1,13 @@
// 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;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
@@ -23,10 +26,17 @@ namespace osu.Game.Tests.Visual.Matchmaking
{
BeatmapSelectPanel? panel = null;
AddStep("add panel", () => Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem())
AddStep("add panel", () =>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
});
AddStep("add maarvin", () => panel!.AddUser(new APIUser
@@ -54,5 +64,41 @@ namespace osu.Game.Tests.Visual.Matchmaking
panel.AllowSelection = value;
});
}
[Test]
public void TestFailedBeatmapLookup()
{
AddStep("setup request handle", () =>
{
var api = (DummyAPIAccess)API;
var handler = api.HandleRequest;
api.HandleRequest = req =>
{
switch (req)
{
case GetBeatmapRequest:
case GetBeatmapsRequest:
req.TriggerFailure(new InvalidOperationException());
return false;
default:
return handler?.Invoke(req) ?? false;
}
};
});
AddStep("add panel", () =>
{
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = new BeatmapSelectPanel(new MultiplayerPlaylistItem())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
});
}
}
}
@@ -0,0 +1,49 @@
// 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.Framework.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
using osuTK;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneMatchmakingChatDisplay : ScreenTestScene
{
private MatchmakingChatDisplay? chat;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("add chat", () =>
{
chat?.Expire();
ScreenFooter.Add(chat = new MatchmakingChatDisplay(new Room())
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Size = new Vector2(700, 130),
Margin = new MarginPadding { Bottom = 10, Right = WaveOverlayContainer.WIDTH_PADDING - OsuScreen.HORIZONTAL_OVERFLOW_PADDING },
Alpha = 0
});
});
AddStep("show footer", () => ScreenFooter.Show());
}
[Test]
public void TestAppearDisappear()
{
AddStep("appear", () => chat!.Appear());
AddWaitStep("wait for animation", 3);
AddStep("disappear", () => chat!.Disappear());
AddWaitStep("wait for animation", 3);
}
}
}
@@ -22,11 +22,11 @@ namespace osu.Game.Tests.Visual.Matchmaking
{
Value =
[
new MatchmakingPool { Id = 0, RulesetId = 0 },
new MatchmakingPool { Id = 1, RulesetId = 1 },
new MatchmakingPool { Id = 2, RulesetId = 2 },
new MatchmakingPool { Id = 3, RulesetId = 3, Variant = 4 },
new MatchmakingPool { Id = 4, RulesetId = 3, Variant = 7 },
new MatchmakingPool { Id = 0, RulesetId = 0, Name = "osu!" },
new MatchmakingPool { Id = 1, RulesetId = 1, Name = "osu!taiko" },
new MatchmakingPool { Id = 2, RulesetId = 2, Name = "osu!catch" },
new MatchmakingPool { Id = 3, RulesetId = 3, Variant = 4, Name = "osu!mania (4k)" },
new MatchmakingPool { Id = 4, RulesetId = 3, Variant = 7, Name = "osu!mania (7k)" },
]
}
});
@@ -120,12 +120,16 @@ namespace osu.Game.Tests.Visual.Matchmaking
changeStage(MatchmakingStage.Ended, state =>
{
int localUserId = API.LocalUser.Value.OnlineID;
int i = 1;
state.Users[localUserId].Placement = 1;
state.Users[localUserId].Rounds[1].Placement = 1;
state.Users[localUserId].Rounds[1].TotalScore = 1;
state.Users[localUserId].Rounds[1].Statistics[HitResult.LargeBonus] = 1;
foreach (var user in MultiplayerClient.ServerRoom!.Users.OrderBy(_ => RNG.Next()))
{
state.Users.GetOrAdd(user.UserID).Placement = i++;
state.Users.GetOrAdd(user.UserID).Points = (8 - i) * 7;
state.Users.GetOrAdd(user.UserID).Rounds.GetOrAdd(1).Placement = 1;
state.Users.GetOrAdd(user.UserID).Rounds.GetOrAdd(1).TotalScore = 1;
state.Users.GetOrAdd(user.UserID).Rounds.GetOrAdd(1).Statistics[HitResult.LargeBonus] = 1;
}
});
}
@@ -13,7 +13,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
{
base.SetUpSteps();
AddStep("add statistic", () => Child = new PanelRoomAward("Statistic description", 1)
AddStep("add award", () => Child = new PanelRoomAward("Award name", "Description of what this award means", 1)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
@@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
Id = 2,
Colour = "99EB47",
CountryCode = CountryCode.AU,
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/2/baba245ef60834b769694178f8f6d4f6166c5188c740de084656ad2b80f1eea7.jpeg",
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
}
})
@@ -102,5 +102,11 @@ namespace osu.Game.Tests.Visual.Matchmaking
{
AddStep("jump", () => MultiplayerClient.SendUserMatchRequest(1, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely());
}
[Test]
public void TestQuit()
{
AddToggleStep("toggle quit", quit => panel.HasQuit = quit);
}
}
}
@@ -118,9 +118,12 @@ namespace osu.Game.Tests.Visual.Matchmaking
});
AddUntilStep("two panels displayed", () => this.ChildrenOfType<PlayerPanel>().Count(), () => Is.EqualTo(2));
AddAssert("no panels quit", () => this.ChildrenOfType<PlayerPanel>().Count(p => p.HasQuit), () => Is.EqualTo(0));
AddStep("remove a user", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 }));
AddUntilStep("one panel displayed", () => this.ChildrenOfType<PlayerPanel>().Count(), () => Is.EqualTo(1));
AddUntilStep("one panel quit", () => this.ChildrenOfType<PlayerPanel>().Count(p => p.HasQuit), () => Is.EqualTo(1));
AddAssert("two panels still displayed", () => this.ChildrenOfType<PlayerPanel>().Count(), () => Is.EqualTo(2));
}
[Test]
@@ -150,7 +153,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
MatchmakingRoomState state = new MatchmakingRoomState();
for (int i = 0; i < room.Users.Count; i++)
state.Users[room.Users[i].UserID].Placement = placements[i];
state.Users.GetOrAdd(room.Users[i].UserID).Placement = placements[i];
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
});
@@ -18,8 +18,6 @@ namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneResultsScreen : MultiplayerTestScene
{
private const int invalid_user_id = 1;
public override void SetUpSteps()
{
base.SetUpSteps();
@@ -27,6 +25,43 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
WaitForJoined();
AddStep("set initial results", () =>
{
var state = new MatchmakingRoomState
{
CurrentRound = 6,
Stage = MatchmakingStage.Ended
};
int localUserId = API.LocalUser.Value.OnlineID;
// Overall state.
state.Users.GetOrAdd(localUserId).Placement = 1;
state.Users.GetOrAdd(localUserId).Points = 8;
for (int round = 1; round <= state.CurrentRound; round++)
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(round).Placement = round;
// Highest score.
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(1).TotalScore = 1000;
// Highest accuracy.
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(2).Accuracy = 0.9995;
// Highest combo.
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(3).MaxCombo = 100;
// Most bonus score.
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(4).Statistics[HitResult.LargeBonus] = 50;
// Smallest score difference.
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(5).TotalScore = 1000;
// Largest score difference.
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(6).TotalScore = 1000;
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
});
AddStep("add results screen", () =>
{
Child = new ScreenStack(new SubScreenResults())
@@ -36,7 +71,18 @@ namespace osu.Game.Tests.Visual.Matchmaking
Size = new Vector2(0.8f)
};
});
}
[Test]
public void TestBasic()
{
AddStep("do nothing", () => { });
}
[Test]
public void TestInvalidUser()
{
const int invalid_user_id = 1;
AddStep("join another user", () => MultiplayerClient.AddUser(new MultiplayerRoomUser(invalid_user_id)
{
User = new APIUser
@@ -45,11 +91,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
Username = "Invalid user"
}
}));
}
[Test]
public void TestResults()
{
AddStep("set results stage", () =>
{
var state = new MatchmakingRoomState
@@ -61,36 +103,51 @@ namespace osu.Game.Tests.Visual.Matchmaking
int localUserId = API.LocalUser.Value.OnlineID;
// Overall state.
state.Users[localUserId].Placement = 1;
state.Users[localUserId].Points = 8;
state.Users[invalid_user_id].Placement = 2;
state.Users[invalid_user_id].Points = 7;
state.Users.GetOrAdd(localUserId).Placement = 1;
state.Users.GetOrAdd(localUserId).Points = 8;
state.Users.GetOrAdd(invalid_user_id).Placement = 2;
state.Users.GetOrAdd(invalid_user_id).Points = 7;
for (int round = 1; round <= state.CurrentRound; round++)
state.Users[localUserId].Rounds[round].Placement = round;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(round).Placement = round;
// Highest score.
state.Users[localUserId].Rounds[1].TotalScore = 1000;
state.Users[invalid_user_id].Rounds[1].TotalScore = 990;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(1).TotalScore = 1000;
state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(1).TotalScore = 990;
// Highest accuracy.
state.Users[localUserId].Rounds[2].Accuracy = 0.9995;
state.Users[invalid_user_id].Rounds[2].Accuracy = 0.5;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(2).Accuracy = 0.9995;
state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(2).Accuracy = 0.5;
// Highest combo.
state.Users[localUserId].Rounds[3].MaxCombo = 100;
state.Users[invalid_user_id].Rounds[3].MaxCombo = 10;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(3).MaxCombo = 100;
state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(3).MaxCombo = 10;
// Most bonus score.
state.Users[localUserId].Rounds[4].Statistics[HitResult.LargeBonus] = 50;
state.Users[invalid_user_id].Rounds[4].Statistics[HitResult.LargeBonus] = 25;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(4).Statistics[HitResult.LargeBonus] = 50;
state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(4).Statistics[HitResult.LargeBonus] = 25;
// Smallest score difference.
state.Users[localUserId].Rounds[5].TotalScore = 1000;
state.Users[invalid_user_id].Rounds[5].TotalScore = 999;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(5).TotalScore = 1000;
state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(5).TotalScore = 999;
// Largest score difference.
state.Users[localUserId].Rounds[6].TotalScore = 1000;
state.Users[invalid_user_id].Rounds[6].TotalScore = 0;
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(6).TotalScore = 1000;
state.Users.GetOrAdd(invalid_user_id).Rounds.GetOrAdd(6).TotalScore = 0;
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
});
}
[Test]
public void TestNoUsers()
{
AddStep("show results with no users", () =>
{
var state = new MatchmakingRoomState
{
CurrentRound = 6,
Stage = MatchmakingStage.Ended
};
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
});
@@ -1,11 +1,14 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Screens;
@@ -54,6 +57,91 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("old back button shown", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Visible));
}
/// <summary>
/// Tests pushing and exiting subscreens that have footers.
/// </summary>
[Test]
public void TestPushAndExitSubScreens()
{
TestScreenWithSubScreen screen = null!;
PushAndConfirm(() => screen = new TestScreenWithSubScreen());
AddAssert("footer hidden", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddAssert("old back button shown", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Visible));
pushSubScreenAndConfirm(() => screen, () => new TestScreenOne());
AddUntilStep("button one shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button One"));
pushSubScreenAndConfirm(() => screen, () => new TestScreenTwo());
AddUntilStep("button two shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button Two"));
AddStep("exit sub screen", () => screen.ExitSubScreen());
AddUntilStep("button one shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button One"));
AddStep("exit sub screen", () => screen.ExitSubScreen());
AddAssert("footer hidden", () => screenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddAssert("old back button shown", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Visible));
}
/// <summary>
/// Tests pushing a new parenting screen while the footer is displayed from a subscreen.
/// </summary>
[Test]
public void TestPushParentScreenDuringSubScreen()
{
TestScreenWithSubScreen screen = null!;
PushAndConfirm(() => screen = new TestScreenWithSubScreen());
pushSubScreenAndConfirm(() => screen, () => new TestScreenOne());
AddUntilStep("button one shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button One"));
PushAndConfirm(() => new TestScreenTwo());
AddUntilStep("button two shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button Two"));
AddStep("exit parent screen", () => Game.ScreenStack.Exit());
AddUntilStep("button one shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button One"));
}
/// <summary>
/// Tests pushing a new subscreen after a new parenting screen has been pushed.
/// </summary>
[Test]
public void TestPushSubScreenWhileNotCurrent()
{
TestScreenWithSubScreen screen = null!;
PushAndConfirm(() => screen = new TestScreenWithSubScreen());
pushSubScreenAndConfirm(() => screen, () => new TestScreenOne());
AddUntilStep("button one shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button One"));
PushAndConfirm(() => new TestScreenOne());
AddUntilStep("button one shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button One"));
// Can't use the helper method because the screen never loads
AddStep("Push new sub screen", () => screen.PushSubScreen(new TestScreenTwo()));
AddWaitStep("wait for potential screen load", 5);
AddUntilStep("button one still shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button One"));
AddStep("exit parent screen", () => Game.ScreenStack.Exit());
AddUntilStep("button two shown", () => screenFooter.ChildrenOfType<ScreenFooterButton>().First().Text.ToString(), () => Is.EqualTo("Button Two"));
}
private void pushSubScreenAndConfirm(Func<TestScreenWithSubScreen> target, Func<Screen> newScreen)
{
Screen screen = null!;
IScreen? previousScreen = null;
AddStep("Push new sub screen", () =>
{
previousScreen = target().CurrentSubScreen;
target().PushSubScreen(screen = newScreen());
});
AddUntilStep("Wait for new screen", () => screen.IsLoaded
&& target().CurrentSubScreen != previousScreen
&& (previousScreen == null || previousScreen.GetChildScreen() == screen));
}
private partial class TestScreenOne : OsuScreen
{
public override bool ShowFooter => true;
@@ -89,5 +177,24 @@ namespace osu.Game.Tests.Visual.Navigation
ShowFooter = footer;
}
}
private partial class TestScreenWithSubScreen : OsuScreen, IHasSubScreenStack
{
public ScreenStack SubScreenStack { get; }
public TestScreenWithSubScreen()
{
InternalChild = SubScreenStack = new ScreenStack
{
RelativeSizeAxes = Axes.Both
};
}
public IScreen? CurrentSubScreen => SubScreenStack.CurrentScreen;
public void PushSubScreen(IScreen screen) => SubScreenStack.Push(screen);
public void ExitSubScreen() => SubScreenStack.Exit();
}
}
}
@@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Online
if (supportLevel > 3)
supportLevel = 0;
((DummyAPIAccess)API).Friends.Add(new APIRelation
((DummyAPIAccess)API).LocalUserState.Friends.Add(new APIRelation
{
TargetID = 2,
RelationType = RelationType.Friend,
@@ -59,8 +59,8 @@ namespace osu.Game.Tests.Visual.Online
AddStep("set friends", () =>
{
DummyAPIAccess api = (DummyAPIAccess)API;
api.Friends.Clear();
api.Friends.AddRange(getUsers().Select(u => new APIRelation
api.LocalUserState.Friends.Clear();
api.LocalUserState.Friends.AddRange(getUsers().Select(u => new APIRelation
{
RelationType = RelationType.Friend,
TargetID = u.OnlineID,
@@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("remove one friend", () =>
{
DummyAPIAccess api = (DummyAPIAccess)API;
api.Friends.RemoveAt(0);
api.LocalUserState.Friends.RemoveAt(0);
});
waitForLoad();
@@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("add one friend", () =>
{
DummyAPIAccess api = (DummyAPIAccess)API;
api.Friends.AddRange(getUsers().Take(1).Select(u => new APIRelation
api.LocalUserState.Friends.AddRange(getUsers().Take(1).Select(u => new APIRelation
{
RelationType = RelationType.Friend,
TargetID = u.OnlineID,
@@ -101,8 +101,8 @@ namespace osu.Game.Tests.Visual.Online
AddStep("set friends", () =>
{
DummyAPIAccess api = (DummyAPIAccess)API;
api.Friends.Clear();
api.Friends.AddRange(getUsers().Select(u => new APIRelation
api.LocalUserState.Friends.Clear();
api.LocalUserState.Friends.AddRange(getUsers().Select(u => new APIRelation
{
RelationType = RelationType.Friend,
TargetID = u.OnlineID,
@@ -130,8 +130,8 @@ namespace osu.Game.Tests.Visual.Online
AddStep("set friends", () =>
{
DummyAPIAccess api = (DummyAPIAccess)API;
api.Friends.Clear();
api.Friends.AddRange(getUsers().Select(u => new APIRelation
api.LocalUserState.Friends.Clear();
api.LocalUserState.Friends.AddRange(getUsers().Select(u => new APIRelation
{
RelationType = RelationType.Friend,
TargetID = u.OnlineID,
@@ -148,7 +148,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("bring a friend online", () =>
{
DummyAPIAccess api = (DummyAPIAccess)API;
metadataClient.FriendPresenceUpdated(api.Friends[0].TargetID, new UserPresence { Status = UserStatus.Online });
metadataClient.FriendPresenceUpdated(api.LocalUserState.Friends[0].TargetID, new UserPresence { Status = UserStatus.Online });
});
assertVisiblePanelCount<UserPanel>(1);
@@ -159,7 +159,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("bring a friend online", () =>
{
DummyAPIAccess api = (DummyAPIAccess)API;
metadataClient.FriendPresenceUpdated(api.Friends[1].TargetID, new UserPresence { Status = UserStatus.Online });
metadataClient.FriendPresenceUpdated(api.LocalUserState.Friends[1].TargetID, new UserPresence { Status = UserStatus.Online });
});
assertVisiblePanelCount<UserPanel>(1);
@@ -170,7 +170,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("take friend offline", () =>
{
DummyAPIAccess api = (DummyAPIAccess)API;
metadataClient.FriendPresenceUpdated(api.Friends[1].TargetID, null);
metadataClient.FriendPresenceUpdated(api.LocalUserState.Friends[1].TargetID, null);
});
assertVisiblePanelCount<UserPanel>(1);
@@ -184,8 +184,8 @@ namespace osu.Game.Tests.Visual.Online
AddStep("set friends", () =>
{
DummyAPIAccess api = (DummyAPIAccess)API;
api.Friends.Clear();
api.Friends.AddRange(getUsers().Select(u => new APIRelation
api.LocalUserState.Friends.Clear();
api.LocalUserState.Friends.AddRange(getUsers().Select(u => new APIRelation
{
RelationType = RelationType.Friend,
TargetID = u.OnlineID,
@@ -471,7 +471,7 @@ namespace osu.Game.Tests.Visual.Online
public DrawableChannel DrawableChannel => InternalChildren.OfType<DrawableChannel>().First();
public ChannelScrollContainer ScrollContainer => (ChannelScrollContainer)((Container)DrawableChannel.Child).Child;
public ChannelScrollContainer ScrollContainer => DrawableChannel.ChildrenOfType<ChannelScrollContainer>().Single();
public FillFlowContainer FillFlow => (FillFlowContainer)ScrollContainer.Child;
@@ -443,7 +443,7 @@ namespace osu.Game.Tests.Visual.Online
Task.Run(() =>
{
requestLock.Wait(3000);
dummyAPI.Friends.Add(apiRelation);
dummyAPI.LocalUserState.Friends.Add(apiRelation);
req.TriggerSuccess(new AddFriendResponse
{
UserRelation = apiRelation
@@ -453,11 +453,11 @@ namespace osu.Game.Tests.Visual.Online
return true;
};
});
AddStep("clear friend list", () => dummyAPI.Friends.Clear());
AddStep("clear friend list", () => dummyAPI.LocalUserState.Friends.Clear());
AddStep("Show non-friend user", () => header.User.Value = new UserProfileData(nonFriend, new OsuRuleset().RulesetInfo));
AddStep("Click followers button", () => this.ChildrenOfType<FollowersButton>().First().TriggerClick());
AddStep("Complete request", () => requestLock.Set());
AddUntilStep("Friend added", () => API.Friends.Any(f => f.TargetID == nonFriend.OnlineID));
AddUntilStep("Friend added", () => API.LocalUserState.Friends.Any(f => f.TargetID == nonFriend.OnlineID));
}
[Test]
@@ -486,7 +486,7 @@ namespace osu.Game.Tests.Visual.Online
Task.Run(() =>
{
requestLock.Wait(3000);
dummyAPI.Friends.Add(apiRelation);
dummyAPI.LocalUserState.Friends.Add(apiRelation);
req.TriggerSuccess(new AddFriendResponse
{
UserRelation = apiRelation
@@ -496,11 +496,11 @@ namespace osu.Game.Tests.Visual.Online
return true;
};
});
AddStep("clear friend list", () => dummyAPI.Friends.Clear());
AddStep("clear friend list", () => dummyAPI.LocalUserState.Friends.Clear());
AddStep("Show non-friend user", () => header.User.Value = new UserProfileData(nonFriend, new OsuRuleset().RulesetInfo));
AddStep("Click followers button", () => this.ChildrenOfType<FollowersButton>().First().TriggerClick());
AddStep("Complete request", () => requestLock.Set());
AddUntilStep("Friend added", () => API.Friends.Any(f => f.TargetID == nonFriend.OnlineID));
AddUntilStep("Friend added", () => API.LocalUserState.Friends.Any(f => f.TargetID == nonFriend.OnlineID));
}
}
}
@@ -352,7 +352,8 @@ namespace osu.Game.Tests.Visual.Ranking
{
Score = userBest,
Position = 133_337,
}
},
ScoresCount = 200_000,
});
return true;
}
@@ -406,7 +407,8 @@ namespace osu.Game.Tests.Visual.Ranking
{
Score = userBest,
Position = 133_337,
}
},
ScoresCount = 200_000,
});
return true;
}
@@ -511,7 +513,8 @@ namespace osu.Game.Tests.Visual.Ranking
{
Score = userBest,
Position = 133_337,
}
},
ScoresCount = 200_000,
});
return true;
}
@@ -271,12 +271,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2
addBeatmapSet(applyStars(2), beatmapSets, out var beatmap2);
addBeatmapSet(applyStars(2.1), beatmapSets, out var beatmapAbove2);
addBeatmapSet(applyStars(7), beatmapSets, out var beatmap7);
addBeatmapSet(applyStars(13), beatmapSets, out var beatmap13);
addBeatmapSet(applyStars(14.996), beatmapSets, out var beatmapAlmost15);
addBeatmapSet(applyStars(15), beatmapSets, out var beatmap15);
addBeatmapSet(applyStars(22), beatmapSets, out var beatmap22);
var results = await runGrouping(GroupMode.Difficulty, beatmapSets);
assertGroup(results, 0, "Below 1 Star", beatmapBelow1.Beatmaps, ref total);
assertGroup(results, 1, "1 Star", (beatmapAbove1.Beatmaps.Concat(beatmapAlmost2.Beatmaps)), ref total);
assertGroup(results, 2, "2 Stars", (beatmap2.Beatmaps.Concat(beatmapAbove2.Beatmaps)), ref total);
assertGroup(results, 3, "7 Stars", beatmap7.Beatmaps, ref total);
assertGroup(results, 4, "13 Stars", beatmap13.Beatmaps, ref total);
assertGroup(results, 5, "14 Stars", beatmapAlmost15.Beatmaps, ref total);
assertGroup(results, 6, "Over 15 Stars", beatmap15.Beatmaps.Concat(beatmap22.Beatmaps), ref total);
assertTotal(results, total);
}
@@ -366,12 +373,40 @@ namespace osu.Game.Tests.Visual.SongSelectV2
#endregion
private static async Task<List<CarouselItem>> runGrouping(GroupMode group, List<BeatmapSetInfo> beatmapSets)
#region Favourites grouping
[Test]
public async Task TestFavouritesGrouping()
{
var groupingFilter = new BeatmapCarouselFilterGrouping(
() => new FilterCriteria { Group = group },
() => new List<BeatmapCollection>(),
_ => new Dictionary<Guid, ScoreRank>());
int total = 0;
var beatmapSets = new List<BeatmapSetInfo>();
addBeatmapSet(s => s.OnlineID = 1, beatmapSets, out _);
addBeatmapSet(s => s.OnlineID = 21, beatmapSets, out var firstFavourite);
addBeatmapSet(s => s.OnlineID = 321, beatmapSets, out _);
addBeatmapSet(s => s.OnlineID = 4321, beatmapSets, out _);
addBeatmapSet(s => s.OnlineID = 54321, beatmapSets, out var secondFavourite);
favouriteBeatmapSets = [21, 54321];
var results = await runGrouping(GroupMode.Favourites, beatmapSets);
assertGroup(results, 0, "Favourites", firstFavourite.Beatmaps.Concat(secondFavourite.Beatmaps), ref total);
assertTotal(results, total);
}
#endregion
private HashSet<int> favouriteBeatmapSets = [];
private async Task<List<CarouselItem>> runGrouping(GroupMode group, List<BeatmapSetInfo> beatmapSets)
{
var groupingFilter = new BeatmapCarouselFilterGrouping
{
GetCriteria = () => new FilterCriteria { Group = group },
GetCollections = () => new List<BeatmapCollection>(),
GetLocalUserTopRanks = _ => new Dictionary<Guid, ScoreRank>(),
GetFavouriteBeatmapSets = () => favouriteBeatmapSets,
};
return await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None);
}
@@ -284,6 +284,32 @@ namespace osu.Game.Tests.Visual.SongSelectV2
CheckHasSelection();
}
[Test]
public void TestSetDoesExpandAgainWhenGroupingTurnedOff()
{
ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title);
CheckDisplayedGroupsCount(1);
CheckDisplayedBeatmapSetsCount(1);
CheckDisplayedBeatmapsCount(3);
CheckHasSelection();
ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty);
CheckDisplayedGroupsCount(5);
CheckDisplayedBeatmapSetsCount(10);
CheckDisplayedBeatmapsCount(30);
ToggleGroupCollapse();
ApplyToFilterAndWaitForFilter("apply no-op filter", c => c.AllowConvertedBeatmaps = !c.AllowConvertedBeatmaps);
AddAssert("group didn't re-expand", () => Carousel.ExpandedGroup, () => Is.Null);
AddAssert("beatmap set didn't re-expand", () => Carousel.GetCarouselItems()!.Count(item => item.Model is GroupedBeatmap && item.IsVisible), () => Is.Zero);
SortAndGroupBy(SortMode.Title, GroupMode.None);
AddAssert("beatmap set did re-expand", () => Carousel.GetCarouselItems()!.Count(item => item.Model is GroupedBeatmap && item.IsVisible), () => Is.Not.Zero);
}
[Test]
public void TestManuallyCollapsingCurrentGroupAndOpeningAnother()
{
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -145,6 +146,62 @@ namespace osu.Game.Tests.Visual.SongSelectV2
}
}
[Test]
public void TestStatuses()
{
foreach (var status in Enum.GetValues<BeatmapOnlineStatus>().Where(s => s != BeatmapOnlineStatus.Approved))
{
AddStep($"display {status} status", () =>
{
ContentContainer.Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine))
},
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 5f),
Children = new[]
{
new PanelGroupRankedStatus
{
Item = new CarouselItem(new RankedStatusGroupDefinition(0, status))
},
new PanelGroupRankedStatus
{
Item = new CarouselItem(new RankedStatusGroupDefinition(1, status)),
KeyboardSelected = { Value = true },
},
new PanelGroupRankedStatus
{
Item = new CarouselItem(new RankedStatusGroupDefinition(2, status)),
Expanded = { Value = true },
},
new PanelGroupRankedStatus
{
Item = new CarouselItem(new RankedStatusGroupDefinition(3, status)),
Expanded = { Value = true },
KeyboardSelected = { Value = true },
},
},
}
}
};
});
}
}
protected override Drawable CreateContent()
{
return new OsuContextMenuContainer
@@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("mods selected", () => SelectedMods.Value, () => Has.Count.EqualTo(1));
AddStep("right click mod button", () =>
{
InputManager.MoveMouseTo(Footer.ChildrenOfType<FooterButtonMods>().Single());
InputManager.MoveMouseTo(ScreenFooter.ChildrenOfType<FooterButtonMods>().Single());
InputManager.Click(MouseButton.Right);
});
AddAssert("not mods selected", () => SelectedMods.Value, () => Has.Count.EqualTo(0));
@@ -620,7 +620,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
}
private FooterButtonRandom randomButton => Footer.ChildrenOfType<FooterButtonRandom>().Single();
private FooterButtonRandom randomButton => ScreenFooter.ChildrenOfType<FooterButtonRandom>().Single();
[Test]
public void TestFooterOptions()
@@ -88,6 +88,33 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("selection unchanged", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.Last()));
}
[Test]
public void TestFilterSingleResult_ReselectedAfterRulesetSwitches()
{
LoadSongSelect();
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
AddStep("disable converts", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
AddStep("set filter text", () => filterTextBox.Current.Value = $"\"{Beatmaps.GetAllUsableBeatmapSets().Last().Metadata.Title}\"");
AddWaitStep("wait for debounce", 5);
AddUntilStep("wait for filter", () => !Carousel.IsFiltering);
AddUntilStep("selection is second beatmap set", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().Last().Beatmaps.First()));
AddStep("select last difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(Beatmap.Value.BeatmapSetInfo.Beatmaps.Last()));
AddUntilStep("selection is last difficulty of second beatmap set", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().Last().Beatmaps.Last()));
ChangeRuleset(1);
AddUntilStep("wait for filter", () => !Carousel.IsFiltering);
AddUntilStep("selection is default", () => Beatmap.IsDefault);
ChangeRuleset(0);
AddUntilStep("wait for filter", () => !Carousel.IsFiltering);
AddUntilStep("selection is last difficulty of second beatmap set", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(Beatmaps.GetAllUsableBeatmapSets().Last().Beatmaps.Last()));
}
[Test]
public void TestFilterOnResumeAfterChange()
{
@@ -50,8 +50,8 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("set 10 friends", () =>
{
DummyAPIAccess api = (DummyAPIAccess)API;
api.Friends.Clear();
api.Friends.AddRange(Enumerable.Range(1, 10).Select(i => new APIRelation
api.LocalUserState.Friends.Clear();
api.LocalUserState.Friends.AddRange(Enumerable.Range(1, 10).Select(i => new APIRelation
{
RelationType = RelationType.Friend,
TargetID = i,
@@ -62,8 +62,8 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("set 20 friends", () =>
{
DummyAPIAccess api = (DummyAPIAccess)API;
api.Friends.Clear();
api.Friends.AddRange(Enumerable.Range(1, 20).Select(i => new APIRelation
api.LocalUserState.Friends.Clear();
api.LocalUserState.Friends.AddRange(Enumerable.Range(1, 20).Select(i => new APIRelation
{
RelationType = RelationType.Friend,
TargetID = i,
@@ -78,8 +78,8 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("set 10 friends", () =>
{
DummyAPIAccess api = (DummyAPIAccess)API;
api.Friends.Clear();
api.Friends.AddRange(Enumerable.Range(1, 10).Select(i => new APIRelation
api.LocalUserState.Friends.Clear();
api.LocalUserState.Friends.AddRange(Enumerable.Range(1, 10).Select(i => new APIRelation
{
RelationType = RelationType.Friend,
TargetID = i,
+14 -5
View File
@@ -65,13 +65,19 @@ namespace osu.Game.Audio
/// </summary>
public bool EditorAutoBank { get; }
public HitSampleInfo(string name, string bank = SampleControlPoint.DEFAULT_BANK, string? suffix = null, int volume = 100, bool editorAutoBank = true)
/// <summary>
/// Whether the sample can be looked up from the beatmap's skin.
/// </summary>
public bool UseBeatmapSamples { get; }
public HitSampleInfo(string name, string bank = SampleControlPoint.DEFAULT_BANK, string? suffix = null, int volume = 100, bool editorAutoBank = true, bool useBeatmapSamples = false)
{
Name = name;
Bank = bank;
Suffix = suffix;
Volume = volume;
EditorAutoBank = editorAutoBank;
UseBeatmapSamples = useBeatmapSamples;
}
/// <summary>
@@ -99,16 +105,19 @@ namespace osu.Game.Audio
/// <param name="newSuffix">An optional new lookup suffix.</param>
/// <param name="newVolume">An optional new volume.</param>
/// <param name="newEditorAutoBank">An optional new editor auto bank flag.</param>
/// <param name="newUseBeatmapSamples">An optional use beatmap samples flag.</param>
/// <returns>The new <see cref="HitSampleInfo"/>.</returns>
public virtual HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default)
=> new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume), newEditorAutoBank.GetOr(EditorAutoBank));
public virtual HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default,
Optional<bool> newEditorAutoBank = default, Optional<bool> newUseBeatmapSamples = default)
=> new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume),
newEditorAutoBank.GetOr(EditorAutoBank), newUseBeatmapSamples.GetOr(UseBeatmapSamples));
public virtual bool Equals(HitSampleInfo? other)
=> other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix;
=> other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix && UseBeatmapSamples == other.UseBeatmapSamples;
public override bool Equals(object? obj)
=> obj is HitSampleInfo other && Equals(other);
public override int GetHashCode() => HashCode.Combine(Name, Bank, Suffix);
public override int GetHashCode() => HashCode.Combine(Name, Bank, Suffix, UseBeatmapSamples);
}
}
+18
View File
@@ -568,6 +568,16 @@ namespace osu.Game.Beatmaps
transaction.Commit();
});
public void MarkNotPlayed(BeatmapInfo beatmapSetInfo) => Realm.Run(r =>
{
using var transaction = r.BeginWrite();
var beatmap = r.Find<BeatmapInfo>(beatmapSetInfo.ID)!;
beatmap.LastPlayed = null;
transaction.Commit();
});
#region Implementation of ICanAcceptFiles
public Task Import(params string[] paths) => beatmapImporter.Import(paths);
@@ -634,6 +644,14 @@ namespace osu.Game.Beatmaps
public override bool IsAvailableLocally(BeatmapSetInfo model) => Realm.Run(realm => realm.All<BeatmapSetInfo>().Any(s => s.OnlineID == model.OnlineID && !s.DeletePending));
public bool IsAvailableLocally(IBeatmapInfo model)
{
return Realm.Run(r => r.All<BeatmapInfo>()
.Filter($@"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false")
.Filter($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", model.OnlineID)
.Any());
}
#endregion
#region Implementation of IPostImports<out BeatmapSetInfo>
@@ -187,6 +187,11 @@ namespace osu.Game.Beatmaps.Drawables
@"1841885 cYsmix - triangles.osz",
// winner of https://osu.ppy.sh/home/news/2023-02-01-twin-trials-contest-beatmapping-phase
@"1971987 James Landino - Aresene's Bazaar.osz",
// locus 2025 https://osu.ppy.sh/home/news/2025-08-21-locus-2025-results
"2412244 Kry.exe - Rift Walker.osz",
"2412260 Koto Spirit - Locus of Hexagram.osz",
"2412232 Will Stetson - Of Our Time.osz",
"2412292 ArXe - Locus Amoenus (feat. Megurine Luka).osz",
};
private static readonly string[] bundled_osu =
@@ -62,6 +62,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
current.Value = new BeatmapSetFavouriteState(favourited, current.Value.FavouriteCount + (favourited ? 1 : -1));
SetLoading(false);
api.LocalUserState.UpdateFavouriteBeatmapSets();
};
favouriteRequest.Failure += e =>
{
@@ -321,9 +321,21 @@ namespace osu.Game.Beatmaps.Formats
int volume = samples.Max(o => o.Volume);
string bank = samples.Where(s => s.Name == HitSampleInfo.HIT_NORMAL).Select(s => s.Bank).FirstOrDefault()
?? samples.Select(s => s.Bank).First();
int customIndex = samples.Any(o => o is ConvertHitObjectParser.LegacyHitSampleInfo)
? samples.OfType<ConvertHitObjectParser.LegacyHitSampleInfo>().Max(o => o.CustomSampleBank)
: -1;
int customIndex = samples.Max(s =>
{
switch (s)
{
case ConvertHitObjectParser.LegacyHitSampleInfo legacy:
return legacy.CustomSampleBank;
default:
if (int.TryParse(s.Suffix, out int index))
return index;
return s.UseBeatmapSamples ? 1 : -1;
}
});
return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = time, SampleVolume = volume, SampleBank = bank, CustomSampleBank = customIndex };
}
+17 -4
View File
@@ -132,7 +132,20 @@ namespace osu.Game.Database
hasPath.Path.ControlPoints[^1].Type = null;
if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1
&& hasPath.Path.ControlPoints[0].Type!.Value.Degree == null) continue;
&& hasPath.Path.ControlPoints[0].Type!.Value.Degree == null)
{
// Round every control point to integer positions before skipping to the next hit object
for (int i = 0; i < hasPath.Path.ControlPoints.Count; i++)
{
var position = new Vector2(
MathF.Round(hasPath.Path.ControlPoints[i].Position.X),
MathF.Round(hasPath.Path.ControlPoints[i].Position.Y));
hasPath.Path.ControlPoints[i].Position = position;
}
continue;
}
var convertedToBezier = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints);
@@ -142,10 +155,10 @@ namespace osu.Game.Database
{
var convertedPoint = convertedToBezier[i];
// Truncate control points to integer positions
// Round control points to integer positions
var position = new Vector2(
(float)Math.Floor(convertedPoint.Position.X),
(float)Math.Floor(convertedPoint.Position.Y));
MathF.Round(convertedPoint.Position.X),
MathF.Round(convertedPoint.Position.Y));
// stable only supports a single curve type specification per slider.
// we exploit the fact that the converted-to-Bézier path only has Bézier segments,
+33 -9
View File
@@ -109,6 +109,8 @@ namespace osu.Game.Database
/// </summary>
private readonly SemaphoreSlim realmRetrievalLock = new SemaphoreSlim(1);
private readonly CountdownEvent pendingAsyncOperations = new CountdownEvent(0);
/// <summary>
/// <c>true</c> when the current thread has already entered the <see cref="realmRetrievalLock"/>.
/// </summary>
@@ -467,6 +469,30 @@ namespace osu.Game.Database
}
}
/// <summary>
/// Run work on realm on a TPL thread, in a way that ensures that the realm isn't disposed before the work is done.
/// </summary>
public Task<T> RunAsync<T>(Func<Realm, T> action, CancellationToken token = default)
{
ObjectDisposedException.ThrowIf(isDisposed, this);
// Required to ensure the read is tracked and accounted for before disposal.
// Can potentially be avoided if we have a need to do so in the future.
if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException($@"{nameof(RunAsync)} must be called from the update thread.");
// CountdownEvent will fail if already at zero.
if (!pendingAsyncOperations.TryAddCount())
pendingAsyncOperations.Reset(1);
return Task.Run(() =>
{
var result = Run(action);
pendingAsyncOperations.Signal();
return result;
}, token);
}
/// <summary>
/// Write changes to realm.
/// </summary>
@@ -507,8 +533,6 @@ namespace osu.Game.Database
}
}
private readonly CountdownEvent pendingAsyncWrites = new CountdownEvent(0);
/// <summary>
/// Write changes to realm asynchronously, guaranteeing order of execution.
/// </summary>
@@ -523,8 +547,8 @@ namespace osu.Game.Database
throw new InvalidOperationException(@$"{nameof(WriteAsync)} must be called from the update thread.");
// CountdownEvent will fail if already at zero.
if (!pendingAsyncWrites.TryAddCount())
pendingAsyncWrites.Reset(1);
if (!pendingAsyncOperations.TryAddCount())
pendingAsyncOperations.Reset(1);
// Regardless of calling Realm.GetInstance or Realm.GetInstanceAsync, there is a blocking overhead on retrieval.
// Adding a forced Task.Run resolves this.
@@ -539,7 +563,7 @@ namespace osu.Game.Database
// ReSharper disable once AccessToDisposedClosure (WriteAsync should be marked as [InstantHandle]).
await realm.WriteAsync(() => action(realm)).ConfigureAwait(false);
pendingAsyncWrites.Signal();
pendingAsyncOperations.Signal();
});
return writeTask;
@@ -559,8 +583,8 @@ namespace osu.Game.Database
throw new InvalidOperationException(@$"{nameof(WriteAsync)} must be called from the update thread.");
// CountdownEvent will fail if already at zero.
if (!pendingAsyncWrites.TryAddCount())
pendingAsyncWrites.Reset(1);
if (!pendingAsyncOperations.TryAddCount())
pendingAsyncOperations.Reset(1);
// Regardless of calling Realm.GetInstance or Realm.GetInstanceAsync, there is a blocking overhead on retrieval.
// Adding a forced Task.Run resolves this.
@@ -576,7 +600,7 @@ namespace osu.Game.Database
// ReSharper disable once AccessToDisposedClosure (WriteAsync should be marked as [InstantHandle]).
result = await realm.WriteAsync(() => action(realm)).ConfigureAwait(false);
pendingAsyncWrites.Signal();
pendingAsyncOperations.Signal();
return result;
});
@@ -1494,7 +1518,7 @@ namespace osu.Game.Database
public void Dispose()
{
if (!pendingAsyncWrites.Wait(10000))
if (!pendingAsyncOperations.Wait(10000))
Logger.Log("Realm took too long waiting on pending async writes", level: LogLevel.Error);
updateRealm?.Dispose();
@@ -13,6 +13,9 @@ namespace osu.Game.Extensions
/// <summary>
/// For a given numeric type, return a formatted string in the standard format we use for display everywhere.
/// </summary>
/// <remarks>
/// Number formatting will abide by <see cref="CultureInfo.CurrentCulture"/>.
/// </remarks>
/// <param name="value">The numeric value.</param>
/// <param name="maxDecimalDigits">The maximum number of decimals to be considered in the original value.</param>
/// <param name="asPercentage">Whether the output should be a percentage. For integer types, 0-100 is mapped to 0-100%; for other types 0-1 is mapped to 0-100%.</param>
@@ -31,12 +34,12 @@ namespace osu.Game.Extensions
if (value is int)
floatValue /= 100;
return floatValue.ToString($@"0.{new string('0', Math.Max(0, significantDigits - 2))}%", CultureInfo.InvariantCulture);
return floatValue.ToString($@"0.{new string('0', Math.Max(0, significantDigits - 2))}%", CultureInfo.CurrentCulture);
}
string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty;
return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}", CultureInfo.InvariantCulture)}";
return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}", CultureInfo.CurrentCulture)}";
}
/// <summary>
+50 -15
View File
@@ -160,7 +160,18 @@ namespace osu.Game.Graphics.Carousel
/// <summary>
/// Scroll carousel to the selected item if available.
/// </summary>
public void ScrollToSelection() => scrollToSelection.Invalidate();
/// <param name="immediate">
/// Whether the scroll position should immediately be shifted to the target, delegating animation to visible panels.
/// This should be true for operations like filtering - where panels are changing visibility state - to avoid large jumps in animation.
/// </param>
public void ScrollToSelection(bool immediate = false)
{
// if an immediate scroll is already requested, don't override it with a slower scroll
if (scrollToSelection == PendingScrollOperation.Immediate)
return;
scrollToSelection = immediate ? PendingScrollOperation.Immediate : PendingScrollOperation.Standard;
}
/// <summary>
/// Returns the vertical spacing between two given carousel items. Negative value can be used to create an overlapping effect.
@@ -400,7 +411,7 @@ namespace osu.Game.Graphics.Carousel
refreshAfterSelection();
if (!Scroll.UserScrolling)
ScrollToSelection();
ScrollToSelection(immediate: true);
NewItemsPresented?.Invoke(carouselItems);
});
@@ -681,6 +692,23 @@ namespace osu.Game.Graphics.Carousel
#endregion
#region Scrolling
/// <summary>
/// Scrolling to selection relies on <see cref="currentKeyboardSelection"/> being fully populated.
/// This flag ensures it runs after <see cref="refreshAfterSelection"/> validates this.
/// </summary>
private PendingScrollOperation scrollToSelection = PendingScrollOperation.None;
private enum PendingScrollOperation
{
None,
Standard,
Immediate,
}
#endregion
#region Audio
private Sample? sampleKeyboardTraversal;
@@ -761,13 +789,26 @@ namespace osu.Game.Graphics.Carousel
{
var item = carouselItems[i];
bool isKeyboardSelection = CheckModelEquality(item.Model, currentKeyboardSelection.Model!);
bool isSelection = CheckModelEquality(item.Model, currentSelection.Model!);
// while we don't know the Y position of the item yet, as it's about to be updated,
// consumers (specifically `BeatmapCarousel.GetSpacingBetweenPanels()`) benefit from `CurrentSelectionItem` already pointing
// at the correct item to avoid redundant local equality checks.
// the Y positions will be filled in after they're computed.
if (isKeyboardSelection)
currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, null, i);
if (isSelection)
currentSelection = new Selection(currentSelection.Model, item, null, i);
updateItemYPosition(item, ref lastVisible, ref yPos);
if (CheckModelEquality(item.Model, currentKeyboardSelection.Model!))
currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, item.CarouselYPosition + item.DrawHeight / 2, i);
if (isKeyboardSelection)
currentKeyboardSelection = currentKeyboardSelection with { YPosition = item.CarouselYPosition + item.DrawHeight / 2 };
if (CheckModelEquality(item.Model, currentSelection.Model!))
currentSelection = new Selection(currentSelection.Model, item, item.CarouselYPosition + item.DrawHeight / 2, i);
if (isSelection)
currentSelection = currentSelection with { YPosition = item.CarouselYPosition + item.DrawHeight / 2 };
}
// Update the total height of all items (to make the scroll container scrollable through the full height even though
@@ -808,12 +849,6 @@ namespace osu.Game.Graphics.Carousel
/// </summary>
private readonly Cached filterReusesPanels = new Cached();
/// <summary>
/// Scrolling to selection relies on <see cref="currentKeyboardSelection"/> being fully populated.
/// This flag ensures it runs after <see cref="refreshAfterSelection"/> validates this.
/// </summary>
private readonly Cached scrollToSelection = new Cached();
protected override void Update()
{
base.Update();
@@ -874,12 +909,12 @@ namespace osu.Game.Graphics.Carousel
{
base.UpdateAfterChildren();
if (!scrollToSelection.IsValid)
if (scrollToSelection != PendingScrollOperation.None)
{
if (GetScrollTarget() is double scrollTarget)
Scroll.ScrollTo(scrollTarget - visibleHalfHeight + BleedTop);
Scroll.ScrollTo(scrollTarget - visibleHalfHeight + BleedTop, animated: scrollToSelection == PendingScrollOperation.Standard);
scrollToSelection.Validate();
scrollToSelection = PendingScrollOperation.None;
}
}
+1 -1
View File
@@ -231,7 +231,7 @@ namespace osu.Game.Graphics
/// Retrieves colour for a <see cref="RankingTier"/>.
/// See https://www.figma.com/file/YHWhp9wZ089YXgB7pe6L1k/Tier-Colours
/// </summary>
public ColourInfo ForRankingTier(RankingTier tier)
public static ColourInfo ForRankingTier(RankingTier tier)
{
switch (tier)
{
+24
View File
@@ -0,0 +1,24 @@
// 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 osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public class BreakInfoStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.BreakInfo";
/// <summary>
/// "Current Progress"
/// </summary>
public static LocalisableString CurrentProgressTitle => new TranslatableString(getKey(@"current_progress_title"), @"Current Progress");
/// <summary>
/// "Grade"
/// </summary>
public static LocalisableString ShowInfoGrade => new TranslatableString(getKey(@"show_info_grade"), @"Grade");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
+5
View File
@@ -194,6 +194,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString Details => new TranslatableString(getKey(@"details"), @"Details...");
/// <summary>
/// "Mapper"
/// </summary>
public static LocalisableString Mapper => new TranslatableString(getKey(@"mapper"), @"Mapper");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
@@ -29,6 +29,61 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString SeekForwardSeconds(double arg0) => new TranslatableString(getKey(@"seek_forward_seconds"), @"Seek forward {0} seconds", arg0);
/// <summary>
/// "Playback speed"
/// </summary>
public static LocalisableString PlaybackSpeed => new TranslatableString(getKey(@"playback_speed"), @"Playback speed");
/// <summary>
/// "Show click markers"
/// </summary>
public static LocalisableString ShowClickMarkers => new TranslatableString(getKey(@"show_click_markers"), @"Show click markers");
/// <summary>
/// "Show frame markers"
/// </summary>
public static LocalisableString ShowFrameMarkers => new TranslatableString(getKey(@"show_frame_markers"), @"Show frame markers");
/// <summary>
/// "Show cursor path"
/// </summary>
public static LocalisableString ShowCursorPath => new TranslatableString(getKey(@"show_cursor_path"), @"Show cursor path");
/// <summary>
/// "Hide gameplay cursor"
/// </summary>
public static LocalisableString HideGameplayCursor => new TranslatableString(getKey(@"hide_gameplay_cursor"), @"Hide gameplay cursor");
/// <summary>
/// "Display length"
/// </summary>
public static LocalisableString DisplayLength => new TranslatableString(getKey(@"display_length"), @"Display length");
/// <summary>
/// "Playback"
/// </summary>
public static LocalisableString PlaybackTitle => new TranslatableString(getKey(@"playback_title"), @"Playback");
/// <summary>
/// "Visual Settings"
/// </summary>
public static LocalisableString VisualSettingsTitle => new TranslatableString(getKey(@"visual_settings_title"), @"Visual Settings");
/// <summary>
/// "Audio Settings"
/// </summary>
public static LocalisableString AudioSettingsTitle => new TranslatableString(getKey(@"audio_settings_title"), @"Audio Settings");
/// <summary>
/// "Input Settings"
/// </summary>
public static LocalisableString InputSettingsTitle => new TranslatableString(getKey(@"input_settings_title"), @"Input Settings");
/// <summary>
/// "Analysis Settings"
/// </summary>
public static LocalisableString AnalysisSettingsTitle => new TranslatableString(getKey(@"analysis_settings_title"), @"Analysis Settings");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
@@ -129,6 +129,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString MarkAsPlayed => new TranslatableString(getKey(@"mark_as_played"), @"Mark as played");
/// <summary>
/// "Remove from played"
/// </summary>
public static LocalisableString RemoveFromPlayed => new TranslatableString(getKey(@"remove_from_played"), @"Remove from played");
/// <summary>
/// "Clear all local scores"
/// </summary>
+13 -109
View File
@@ -6,7 +6,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
@@ -18,7 +17,7 @@ using osu.Framework.Development;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Game.Configuration;
using osu.Game.Localisation;
@@ -26,11 +25,10 @@ using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Users;
namespace osu.Game.Online.API
{
public partial class APIAccess : Component, IAPIProvider
public partial class APIAccess : CompositeComponent, IAPIProvider
{
private readonly OsuGameBase game;
private readonly OsuConfigManager config;
@@ -53,30 +51,23 @@ namespace osu.Game.Online.API
public string ProvidedUsername { get; private set; }
public SessionVerificationMethod? SessionVerificationMethod { get; set; }
public SessionVerificationMethod? SessionVerificationMethod { get; private set; }
public string SecondFactorCode { get; private set; }
private string password;
public IBindable<APIUser> LocalUser => localUser;
public IBindableList<APIRelation> Friends => friends;
public IBindableList<APIRelation> Blocks => blocks;
public IBindable<APIUser> LocalUser => localUserState.User;
public ILocalUserState LocalUserState => localUserState;
private readonly LocalUserState localUserState;
public INotificationsClient NotificationsClient { get; }
public Language Language => game.CurrentLanguage.Value;
private Bindable<APIUser> localUser { get; } = new Bindable<APIUser>(createGuestUser());
private BindableList<APIRelation> friends { get; } = new BindableList<APIRelation>();
private BindableList<APIRelation> blocks { get; } = new BindableList<APIRelation>();
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
private readonly Bindable<UserStatus> configStatus = new Bindable<UserStatus>();
private readonly Bindable<bool> configSupporter = new Bindable<bool>();
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
private readonly Logger log;
@@ -108,13 +99,12 @@ namespace osu.Game.Online.API
authentication.TokenString = config.Get<string>(OsuSetting.Token);
authentication.Token.ValueChanged += onTokenChanged;
config.BindWith(OsuSetting.UserOnlineStatus, configStatus);
config.BindWith(OsuSetting.WasSupporter, configSupporter);
AddInternal(localUserState = new LocalUserState(this, config));
if (HasLogin)
{
// Early call to ensure the local user / "logged in" state is correct immediately.
setPlaceholderLocalUser();
localUserState.SetPlaceholderLocalUser(ProvidedUsername);
// This is required so that Queue() requests during startup sequence don't fail due to "not logged in".
state.Value = APIState.Connecting;
@@ -249,8 +239,8 @@ namespace osu.Game.Online.API
/// <returns>Whether the connection attempt was successful.</returns>
private void attemptConnect()
{
if (localUser.IsDefault)
Scheduler.Add(setPlaceholderLocalUser, false);
if (LocalUser.IsDefault)
Scheduler.Add(localUserState.SetPlaceholderLocalUser, ProvidedUsername, false);
// save the username at this point, if the user requested for it to be.
config.SetValue(OsuSetting.Username, config.Get<bool>(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty);
@@ -348,8 +338,7 @@ namespace osu.Game.Online.API
{
Debug.Assert(ThreadSafety.IsUpdateThread);
localUser.Value = me;
configSupporter.Value = me.IsSupporter;
localUserState.SetLocalUser(me);
SessionVerificationMethod = me.SessionVerificationMethod;
state.Value = SessionVerificationMethod == null ? APIState.Online : APIState.RequiresSecondFactorAuth;
failureCount = 0;
@@ -365,8 +354,6 @@ namespace osu.Game.Online.API
}
}
UpdateLocalFriends();
// The Success callback event is fired on the main thread, so we should wait for that to run before proceeding.
// Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests
// before actually going online.
@@ -374,23 +361,6 @@ namespace osu.Game.Online.API
Thread.Sleep(500);
}
/// <summary>
/// Show a placeholder user if saved credentials are available.
/// This is useful for storing local scores and showing a placeholder username after starting the game,
/// until a valid connection has been established.
/// </summary>
private void setPlaceholderLocalUser()
{
if (!localUser.IsDefault)
return;
localUser.Value = new APIUser
{
Username = ProvidedUsername,
IsSupporter = configSupporter.Value,
};
}
public void Perform(APIRequest request)
{
try
@@ -619,78 +589,12 @@ namespace osu.Game.Online.API
SecondFactorCode = null;
authentication.Clear();
// Reset the status to be broadcast on the next login, in case multiple players share the same system.
configStatus.Value = UserStatus.Online;
// Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present
Schedule(() =>
{
localUser.Value = createGuestUser();
configSupporter.Value = false;
friends.Clear();
});
localUserState.ClearLocalUser();
state.Value = APIState.Offline;
flushQueue();
}
public void UpdateLocalFriends()
{
if (!IsLoggedIn)
return;
var friendsReq = new GetFriendsRequest();
friendsReq.Failure += ex =>
{
if (ex is not WebRequestFlushedException)
state.Value = APIState.Failing;
};
friendsReq.Success += res =>
{
var existingFriends = friends.Select(f => f.TargetID).ToHashSet();
var updatedFriends = res.Select(f => f.TargetID).ToHashSet();
// Add new friends into local list.
friends.AddRange(res.Where(r => !existingFriends.Contains(r.TargetID)));
// Remove non-friends from local list.
friends.RemoveAll(f => !updatedFriends.Contains(f.TargetID));
};
Queue(friendsReq);
}
public void UpdateLocalBlocks()
{
if (!IsLoggedIn)
return;
var blocksReq = new GetBlocksRequest();
blocksReq.Failure += ex =>
{
if (ex is not WebRequestFlushedException)
state.Value = APIState.Failing;
};
blocksReq.Success += res =>
{
var existingBlocks = blocks.Select(f => f.TargetID).ToHashSet();
var updatedBlocks = res.Select(f => f.TargetID).ToHashSet();
// Add new blocked users to local list.
blocks.AddRange(res.Where(r => !existingBlocks.Contains(r.TargetID)));
// Remove non-blocked users from local list.
blocks.RemoveAll(b => !updatedBlocks.Contains(b.TargetID));
// Remove friends who got blocked since last check.
friends.RemoveAll(f => updatedBlocks.Contains(f.TargetID));
};
Queue(blocksReq);
}
private static APIUser createGuestUser() => new GuestUser();
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
+34 -11
View File
@@ -20,14 +20,11 @@ namespace osu.Game.Online.API
{
public const int DUMMY_USER_ID = 1001;
public Bindable<APIUser> LocalUser { get; } = new Bindable<APIUser>(new APIUser
{
Username = @"Local user",
Id = DUMMY_USER_ID,
});
public DummyLocalUserState LocalUserState { get; } = new DummyLocalUserState();
public Bindable<APIUser> LocalUser => LocalUserState.User;
public BindableList<APIRelation> Friends { get; } = new BindableList<APIRelation>();
public BindableList<APIRelation> Blocks { get; } = new BindableList<APIRelation>();
ILocalUserState IAPIProvider.LocalUserState => LocalUserState;
IBindable<APIUser> IAPIProvider.LocalUser => LocalUser;
public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient();
INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient;
@@ -208,10 +205,6 @@ namespace osu.Game.Online.API
public void SetState(APIState newState) => state.Value = newState;
IBindable<APIUser> IAPIProvider.LocalUser => LocalUser;
IBindableList<APIRelation> IAPIProvider.Friends => Friends;
IBindableList<APIRelation> IAPIProvider.Blocks => Blocks;
/// <summary>
/// Skip 2FA requirement for next login.
/// </summary>
@@ -234,5 +227,35 @@ namespace osu.Game.Online.API
// Ensure (as much as we can) that any pending tasks are run.
Scheduler.Update();
}
public class DummyLocalUserState : ILocalUserState
{
public Bindable<APIUser> User { get; } = new Bindable<APIUser>(new APIUser
{
Username = @"Local user",
Id = DUMMY_USER_ID,
});
public BindableList<APIRelation> Friends { get; } = new BindableList<APIRelation>();
public BindableList<APIRelation> Blocks { get; } = new BindableList<APIRelation>();
public BindableList<int> FavouriteBeatmapSets { get; } = new BindableList<int>();
IBindable<APIUser> ILocalUserState.User => User;
IBindableList<APIRelation> ILocalUserState.Friends => Friends;
IBindableList<APIRelation> ILocalUserState.Blocks => Blocks;
IBindableList<int> ILocalUserState.FavouriteBeatmapSets => FavouriteBeatmapSets;
public void UpdateFriends()
{
}
public void UpdateBlocks()
{
}
public void UpdateFavouriteBeatmapSets()
{
}
}
}
}
+4 -17
View File
@@ -19,14 +19,11 @@ namespace osu.Game.Online.API
IBindable<APIUser> LocalUser { get; }
/// <summary>
/// The user's friends.
/// The local user's current state.
/// Contains auxiliary information such as the user's friends, blocks, and favourites,
/// as well as methods to manage those in a way that keeps this state consistent throughout the game.
/// </summary>
IBindableList<APIRelation> Friends { get; }
/// <summary>
/// The users blocked by the local user.
/// </summary>
IBindableList<APIRelation> Blocks { get; }
ILocalUserState LocalUserState { get; }
/// <summary>
/// The language supplied by this provider to API requests.
@@ -123,16 +120,6 @@ namespace osu.Game.Online.API
/// </summary>
void Logout();
/// <summary>
/// Update the friends status of the current user.
/// </summary>
void UpdateLocalFriends();
/// <summary>
/// Update the list of users blocked by the current user.
/// </summary>
void UpdateLocalBlocks();
/// <summary>
/// Schedule a callback to run on the update thread.
/// </summary>
+20
View File
@@ -0,0 +1,20 @@
// 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 osu.Framework.Bindables;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API
{
public interface ILocalUserState
{
IBindable<APIUser> User { get; }
IBindableList<APIRelation> Friends { get; }
IBindableList<APIRelation> Blocks { get; }
IBindableList<int> FavouriteBeatmapSets { get; }
void UpdateFriends();
void UpdateBlocks();
void UpdateFavouriteBeatmapSets();
}
}
+151
View File
@@ -0,0 +1,151 @@
// 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.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Users;
namespace osu.Game.Online.API
{
public partial class LocalUserState : Component, ILocalUserState
{
public IBindable<APIUser> User => localUser;
public IBindableList<APIRelation> Friends => friends;
public IBindableList<APIRelation> Blocks => blocks;
public IBindableList<int> FavouriteBeatmapSets => favouriteBeatmapSets;
private readonly IAPIProvider api;
private readonly Bindable<APIUser> localUser = new Bindable<APIUser>(createGuestUser());
private readonly BindableList<APIRelation> friends = new BindableList<APIRelation>();
private readonly BindableList<APIRelation> blocks = new BindableList<APIRelation>();
private readonly BindableList<int> favouriteBeatmapSets = new BindableList<int>();
private readonly Bindable<UserStatus> configStatus = new Bindable<UserStatus>();
private readonly Bindable<bool> configSupporter = new Bindable<bool>();
public LocalUserState(IAPIProvider api, OsuConfigManager config)
{
this.api = api;
config.BindWith(OsuSetting.UserOnlineStatus, configStatus);
config.BindWith(OsuSetting.WasSupporter, configSupporter);
}
#region Logging in / out
private static APIUser createGuestUser() => new GuestUser();
/// <summary>
/// Show a placeholder user if saved credentials are available.
/// This is useful for storing local scores and showing a placeholder username after starting the game,
/// until a valid connection has been established.
/// </summary>
public void SetPlaceholderLocalUser(string username)
{
if (!localUser.IsDefault)
return;
localUser.Value = new APIUser
{
Username = username,
IsSupporter = configSupporter.Value,
};
}
public void SetLocalUser(APIMe me)
{
localUser.Value = me;
configSupporter.Value = me.IsSupporter;
UpdateFriends();
UpdateBlocks();
UpdateFavouriteBeatmapSets();
}
public void ClearLocalUser()
{
// Reset the status to be broadcast on the next login, in case multiple players share the same system.
configStatus.Value = UserStatus.Online;
// Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present
Schedule(() =>
{
localUser.Value = createGuestUser();
configSupporter.Value = false;
friends.Clear();
blocks.Clear();
favouriteBeatmapSets.Clear();
});
}
#endregion
public void UpdateFriends()
{
if (!api.IsLoggedIn)
return;
var friendsReq = new GetFriendsRequest();
friendsReq.Success += res =>
{
var existingFriends = friends.Select(f => f.TargetID).ToHashSet();
var updatedFriends = res.Select(f => f.TargetID).ToHashSet();
// Add new friends into local list.
friends.AddRange(res.Where(r => !existingFriends.Contains(r.TargetID)));
// Remove non-friends from local list.
friends.RemoveAll(f => !updatedFriends.Contains(f.TargetID));
};
api.Queue(friendsReq);
}
public void UpdateBlocks()
{
if (!api.IsLoggedIn)
return;
var blocksReq = new GetBlocksRequest();
blocksReq.Success += res =>
{
var existingBlocks = blocks.Select(f => f.TargetID).ToHashSet();
var updatedBlocks = res.Select(f => f.TargetID).ToHashSet();
// Add new blocked users to local list.
blocks.AddRange(res.Where(r => !existingBlocks.Contains(r.TargetID)));
// Remove non-blocked users from local list.
blocks.RemoveAll(b => !updatedBlocks.Contains(b.TargetID));
// Remove friends who got blocked since last check.
friends.RemoveAll(f => updatedBlocks.Contains(f.TargetID));
};
api.Queue(blocksReq);
}
public void UpdateFavouriteBeatmapSets()
{
if (!api.IsLoggedIn)
return;
var favouritesReq = new GetMyFavouriteBeatmapSetsRequest();
favouritesReq.Success += res =>
{
var existingBeatmapSets = favouriteBeatmapSets.ToHashSet();
var updatedBeatmapSets = res.BeatmapSetIds.ToHashSet();
favouriteBeatmapSets.AddRange(updatedBeatmapSets.Except(existingBeatmapSets));
favouriteBeatmapSets.RemoveAll(b => !updatedBeatmapSets.Contains(b));
};
api.Queue(favouritesReq);
}
}
}
@@ -0,0 +1,12 @@
// 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 osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
public class GetMyFavouriteBeatmapSetsRequest : APIRequest<GetMyFavouriteBeatmapSetsResponse>
{
protected override string Target => @"me/beatmapset-favourites";
}
}
@@ -20,6 +20,8 @@ namespace osu.Game.Online.API.Requests
public const int DEFAULT_SCORES_PER_REQUEST = 50;
public const int MAX_SCORES_PER_REQUEST = 100;
public int ScoresRequested { get; }
private readonly IBeatmapInfo beatmapInfo;
private readonly BeatmapLeaderboardScope scope;
private readonly IRulesetInfo ruleset;
@@ -37,6 +39,8 @@ namespace osu.Game.Online.API.Requests
this.scope = scope;
this.ruleset = ruleset ?? throw new ArgumentNullException(nameof(ruleset));
this.mods = mods ?? Array.Empty<IMod>();
ScoresRequested = this.scope.RequiresSupporter(this.mods.Any()) ? MAX_SCORES_PER_REQUEST : DEFAULT_SCORES_PER_REQUEST;
}
protected override string Target => $@"beatmaps/{beatmapInfo.OnlineID}/scores";
@@ -51,7 +55,7 @@ namespace osu.Game.Online.API.Requests
foreach (var mod in mods)
req.AddParameter(@"mods[]", mod.Acronym);
req.AddParameter(@"limit", (scope.RequiresSupporter(mods.Any()) ? MAX_SCORES_PER_REQUEST : DEFAULT_SCORES_PER_REQUEST).ToString(CultureInfo.InvariantCulture));
req.AddParameter(@"limit", ScoresRequested.ToString(CultureInfo.InvariantCulture));
return req;
}
@@ -0,0 +1,13 @@
// 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 Newtonsoft.Json;
namespace osu.Game.Online.API.Requests.Responses
{
public class GetMyFavouriteBeatmapSetsResponse
{
[JsonProperty("beatmapset_ids")]
public int[] BeatmapSetIds { get; set; } = [];
}
}
+1 -1
View File
@@ -53,7 +53,7 @@ namespace osu.Game.Online
config.BindWith(OsuSetting.NotifyOnFriendPresenceChange, notifyOnFriendPresenceChange);
friends.BindTo(api.Friends);
friends.BindTo(api.LocalUserState.Friends);
friends.BindCollectionChanged(onFriendsChanged, true);
friendPresences.BindTo(metadataClient.FriendPresences);
@@ -144,7 +144,8 @@ namespace osu.Game.Online.Leaderboards
return s;
})
.ToArray(),
response.ScoresCount,
scoresRequested: newRequest.ScoresRequested,
totalScores: response.ScoresCount,
response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap)
);
inFlightOnlineRequest = null;
@@ -194,7 +195,7 @@ namespace osu.Game.Online.Leaderboards
newScores = newScores.Detach().OrderByCriteria(CurrentCriteria.Sorting);
var newScoresArray = newScores.ToArray();
scores.Value = LeaderboardScores.Success(newScoresArray, newScoresArray.Length, null);
scores.Value = LeaderboardScores.Success(newScoresArray, scoresRequested: newScoresArray.Length, totalScores: newScoresArray.Length, null);
}
protected override void Dispose(bool isDisposing)
@@ -215,9 +216,33 @@ namespace osu.Game.Online.Leaderboards
public record LeaderboardScores
{
/// <summary>
/// The collection of all scores received through the leaderboard lookup.
/// </summary>
public ICollection<ScoreInfo> TopScores { get; }
/// <summary>
/// The number of scores which was requested.
/// Used to determine whether the returned leaderboard can be judged to be a partial or full leaderboard
/// (i.e. whether <see cref="TopScores"/> contains all scores that it could ever contain).
/// </summary>
public int ScoresRequested { get; }
/// <summary>
/// The number of all scores that exist on the leaderboard.
/// </summary>
public int TotalScores { get; }
public bool IsPartial => ScoresRequested < TotalScores;
/// <summary>
/// The local user's best score.
/// </summary>
public ScoreInfo? UserScore { get; }
/// <summary>
/// The failure state that occurred when attempting to retrieve the leaderboard.
/// </summary>
public LeaderboardFailState? FailState { get; }
public IEnumerable<ScoreInfo> AllScores
@@ -232,16 +257,20 @@ namespace osu.Game.Online.Leaderboards
}
}
private LeaderboardScores(ICollection<ScoreInfo> topScores, int totalScores, ScoreInfo? userScore, LeaderboardFailState? failState)
private LeaderboardScores(ICollection<ScoreInfo> topScores, int scoresRequested, int totalScores, ScoreInfo? userScore, LeaderboardFailState? failState)
{
TopScores = topScores;
ScoresRequested = scoresRequested;
TotalScores = totalScores;
UserScore = userScore;
FailState = failState;
}
public static LeaderboardScores Success(ICollection<ScoreInfo> topScores, int totalScores, ScoreInfo? userScore) => new LeaderboardScores(topScores, totalScores, userScore, null);
public static LeaderboardScores Failure(LeaderboardFailState failState) => new LeaderboardScores([], 0, null, failState);
public static LeaderboardScores Success(ICollection<ScoreInfo> topScores, int scoresRequested, int totalScores, ScoreInfo? userScore)
=> new LeaderboardScores(topScores, scoresRequested, totalScores, userScore, null);
public static LeaderboardScores Failure(LeaderboardFailState failState)
=> new LeaderboardScores([], scoresRequested: 0, totalScores: 0, null, failState);
}
public enum LeaderboardFailState
@@ -103,7 +103,7 @@ namespace osu.Game.Online.Leaderboards
private void load(IAPIProvider api, OsuColour colour)
{
var user = Score.User;
bool isUserFriend = api.Friends.Any(friend => friend.TargetID == user.OnlineID);
bool isUserFriend = api.LocalUserState.Friends.Any(friend => friend.TargetID == user.OnlineID);
statisticsLabels = GetStatistics(Score).Select(s => new ScoreComponentLabel(s)).ToList();
@@ -89,13 +89,13 @@ namespace osu.Game.Online.Metadata
userStatus.BindValueChanged(status =>
{
if (localUser.Value is not GuestUser)
UpdateStatus(status.NewValue);
UpdateStatus(status.NewValue).FireAndForget();
}, true);
userActivity.BindValueChanged(activity =>
{
if (localUser.Value is not GuestUser)
UpdateActivity(activity.NewValue);
UpdateActivity(activity.NewValue).FireAndForget();
}, true);
}
@@ -121,8 +121,8 @@ namespace osu.Game.Online.Metadata
if (localUser.Value is not GuestUser)
{
UpdateActivity(userActivity.Value);
UpdateStatus(userStatus.Value);
UpdateActivity(userActivity.Value).FireAndForget();
UpdateStatus(userStatus.Value).FireAndForget();
}
if (lastQueueId.Value >= 0)
@@ -81,10 +81,10 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking
foreach (var score in scoreGroup)
{
MatchmakingUser mmUser = Users[score.UserID];
MatchmakingUser mmUser = Users.GetOrAdd(score.UserID);
mmUser.Points += placementPoints[placement - 1];
MatchmakingRound mmRound = mmUser.Rounds[CurrentRound];
MatchmakingRound mmRound = mmUser.Rounds.GetOrAdd(CurrentRound);
mmRound.Placement = placement;
mmRound.TotalScore = score.TotalScore;
mmRound.Accuracy = score.Accuracy;
@@ -21,27 +21,24 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking
[Key(0)]
public IDictionary<int, MatchmakingRound> RoundsDictionary { get; set; } = new Dictionary<int, MatchmakingRound>();
/// <summary>
/// Creates or retrieves the score for the given round.
/// </summary>
/// <param name="round">The round.</param>
public MatchmakingRound this[int round]
{
get
{
if (RoundsDictionary.TryGetValue(round, out MatchmakingRound? score))
return score;
return RoundsDictionary[round] = new MatchmakingRound { Round = round };
}
}
/// <summary>
/// The total number of rounds.
/// </summary>
[IgnoreMember]
public int Count => RoundsDictionary.Count;
/// <summary>
/// Retrieves or adds a <see cref="MatchmakingRound"/> entry to this list.
/// </summary>
/// <param name="round">The round.</param>
public MatchmakingRound GetOrAdd(int round)
{
if (RoundsDictionary.TryGetValue(round, out MatchmakingRound? score))
return score;
return RoundsDictionary[round] = new MatchmakingRound { Round = round };
}
public IEnumerator<MatchmakingRound> GetEnumerator() => RoundsDictionary.Values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
@@ -23,7 +23,7 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking
/// The aggregate room placement (1-based).
/// </summary>
[Key(1)]
public int Placement { get; set; }
public int? Placement { get; set; }
/// <summary>
/// The aggregate points.
@@ -21,27 +21,24 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking
[Key(0)]
public IDictionary<int, MatchmakingUser> UserDictionary { get; set; } = new Dictionary<int, MatchmakingUser>();
/// <summary>
/// Creates or retrieves the user for the given id.
/// </summary>
/// <param name="userId">The user id.</param>
public MatchmakingUser this[int userId]
{
get
{
if (UserDictionary.TryGetValue(userId, out MatchmakingUser? user))
return user;
return UserDictionary[userId] = new MatchmakingUser { UserId = userId };
}
}
/// <summary>
/// The total number of users.
/// </summary>
[IgnoreMember]
public int Count => UserDictionary.Count;
/// <summary>
/// Retrieves or adds a <see cref="MatchmakingUser"/> entry to this list.
/// </summary>
/// <param name="userId">The user ID.</param>
public MatchmakingUser GetOrAdd(int userId)
{
if (UserDictionary.TryGetValue(userId, out MatchmakingUser? user))
return user;
return UserDictionary[userId] = new MatchmakingUser { UserId = userId };
}
public IEnumerator<MatchmakingUser> GetEnumerator() => UserDictionary.Values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
@@ -201,7 +201,7 @@ namespace osu.Game.Online.Multiplayer
if (!connected.NewValue)
{
if (Room != null)
LeaveRoom();
LeaveRoom().FireAndForget();
MatchmakingQueueLeft?.Invoke();
}
@@ -560,7 +560,7 @@ namespace osu.Game.Online.Multiplayer
return;
if (user.Equals(LocalUser))
LeaveRoom();
LeaveRoom().FireAndForget();
handleUserLeft(user, UserKicked);
});
+5 -4
View File
@@ -14,6 +14,7 @@ using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
@@ -203,7 +204,7 @@ namespace osu.Game.Online.Spectator
Task IStatefulUserHubClient.DisconnectRequested()
{
Schedule(() => DisconnectInternal());
Schedule(() => DisconnectInternal().FireAndForget());
return Task.CompletedTask;
}
@@ -290,7 +291,7 @@ namespace osu.Game.Online.Spectator
else
currentState.State = SpectatedUserState.Quit;
EndPlayingInternal(currentState);
EndPlayingInternal(currentState).FireAndForget();
});
}
@@ -304,7 +305,7 @@ namespace osu.Game.Online.Spectator
return;
}
WatchUserInternal(userId);
WatchUserInternal(userId).FireAndForget();
}
public void StopWatchingUser(int userId)
@@ -321,7 +322,7 @@ namespace osu.Game.Online.Spectator
watchedUsersRefCounts.Remove(userId);
watchedUserStates.Remove(userId);
StopWatchingUserInternal(userId);
StopWatchingUserInternal(userId).FireAndForget();
});
}
+6 -63
View File
@@ -189,19 +189,14 @@ namespace osu.Game
/// </summary>
public readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>();
/// <summary>
/// Whether the back button is currently displayed.
/// </summary>
private readonly IBindable<bool> backButtonVisibility = new BindableBool();
IBindable<LocalUserPlayingState> ILocalUserPlayInfo.PlayingState => UserPlayingState;
protected readonly Bindable<LocalUserPlayingState> UserPlayingState = new Bindable<LocalUserPlayingState>();
protected OsuScreenStack ScreenStack;
protected BackButton BackButton;
protected ScreenFooter ScreenFooter;
protected BackButton BackButton => screenStackFooter.BackButton;
protected ScreenFooter ScreenFooter => screenStackFooter.Footer;
protected SettingsOverlay Settings;
@@ -233,6 +228,8 @@ namespace osu.Game
private RealmDetachedBeatmapStore detachedBeatmapStore;
private ScreenStackFooter screenStackFooter;
private readonly string[] args;
private readonly List<OsuFocusedOverlayContainer> focusedOverlays = new List<OsuFocusedOverlayContainer>();
@@ -1132,12 +1129,6 @@ namespace osu.Game
{
backReceptor = new ScreenFooter.BackReceptor(),
ScreenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both },
BackButton = new BackButton(backReceptor)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Action = handleBackButton,
},
logoContainer = new Container { RelativeSizeAxes = Axes.Both },
// TODO: what is this? why is this?
// TODO: this is being screen scaled even though it's probably AN OVERLAY.
@@ -1150,7 +1141,7 @@ namespace osu.Game
{
Depth = -1,
RelativeSizeAxes = Axes.Both,
Child = ScreenFooter = new ScreenFooter(backReceptor)
Child = screenStackFooter = new ScreenStackFooter(ScreenStack, backReceptor)
{
// TODO: this is really really weird and should not exist.
RequestLogoInFront = inFront => ScreenContainer.ChangeChildDepth(logoContainer, inFront ? float.MinValue : 0),
@@ -1324,14 +1315,6 @@ namespace osu.Game
if (mode.NewValue != OverlayActivation.All) CloseAllOverlays();
};
backButtonVisibility.ValueChanged += visible =>
{
if (visible.NewValue)
BackButton.Show();
else
BackButton.Hide();
};
// Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup.
handleStartupImport();
}
@@ -1723,13 +1706,12 @@ namespace osu.Game
if (current != null)
{
backButtonVisibility.UnbindFrom(current.BackButtonVisibility);
OverlayActivationMode.UnbindFrom(current.OverlayActivationMode);
configUserActivity.UnbindFrom(current.Activity);
}
// Bind to new screen.
if (newScreen != null)
if (newScreen is OsuScreen newOsuScreen)
{
OverlayActivationMode.BindTo(newScreen.OverlayActivationMode);
configUserActivity.BindTo(newScreen.Activity);
@@ -1742,45 +1724,6 @@ namespace osu.Game
else
Toolbar.Show();
var newOsuScreen = (OsuScreen)newScreen;
if (newScreen.ShowFooter)
{
// the legacy back button should never display while the new footer is in use, as it
// contains its own local back button.
((BindableBool)backButtonVisibility).Value = false;
BackButton.Hide();
ScreenFooter.Show();
if (newOsuScreen.IsLoaded)
updateFooterButtons();
else
{
// ensure the current buttons are immediately disabled on screen change (so they can't be pressed).
ScreenFooter.SetButtons(Array.Empty<ScreenFooterButton>());
newOsuScreen.OnLoadComplete += _ => updateFooterButtons();
}
void updateFooterButtons()
{
var buttons = newScreen.CreateFooterButtons();
newOsuScreen.LoadComponentsAgainstScreenDependencies(buttons);
ScreenFooter.SetButtons(buttons);
ScreenFooter.Show();
}
}
else
{
backButtonVisibility.BindTo(newScreen.BackButtonVisibility);
ScreenFooter.SetButtons(Array.Empty<ScreenFooterButton>());
ScreenFooter.Hide();
}
skinEditor.SetTarget(newOsuScreen);
}
}
@@ -74,6 +74,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
{
favourited.Toggle();
loading.Hide();
api.LocalUserState.UpdateFavouriteBeatmapSets();
};
request.Failure += e =>
+11 -17
View File
@@ -12,7 +12,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Online.Chat;
using osuTK.Graphics;
@@ -49,25 +48,20 @@ namespace osu.Game.Overlays.Chat
[BackgroundDependencyLoader]
private void load()
{
Child = new OsuContextMenuContainer
Child = scroll = new ChannelScrollContainer
{
ScrollbarVisible = scrollbarVisible,
RelativeSizeAxes = Axes.Both,
Masking = true,
Child = scroll = new ChannelScrollContainer
// Some chat lines have effects that slightly protrude to the bottom,
// which we do not want to mask away, hence the padding.
Padding = new MarginPadding { Bottom = 5 },
Child = ChatLineFlow = new FillFlowContainer
{
ScrollbarVisible = scrollbarVisible,
RelativeSizeAxes = Axes.Both,
// Some chat lines have effects that slightly protrude to the bottom,
// which we do not want to mask away, hence the padding.
Padding = new MarginPadding { Bottom = 5 },
Child = ChatLineFlow = new FillFlowContainer
{
Padding = new MarginPadding { Left = 3, Right = 10 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
}
},
Padding = new MarginPadding { Left = 3, Right = 10 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
}
};
newMessagesArrived(Channel.Messages);
@@ -212,7 +212,7 @@ namespace osu.Game.Overlays.Chat
items.Add(new OsuMenuItemSpacer());
items.Add(new OsuMenuItem(UsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested));
items.Add(api.Blocks.Any(b => b.TargetID == user.OnlineID)
items.Add(api.LocalUserState.Blocks.Any(b => b.TargetID == user.OnlineID)
? new OsuMenuItem(UsersStrings.BlocksButtonUnblock, MenuItemType.Standard, () => dialogOverlay?.Push(ConfirmBlockActionDialog.Unblock(user)))
: new OsuMenuItem(UsersStrings.BlocksButtonBlock, MenuItemType.Destructive, () => dialogOverlay?.Push(ConfirmBlockActionDialog.Block(user))));
+6 -1
View File
@@ -19,6 +19,7 @@ using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online;
@@ -142,9 +143,13 @@ namespace osu.Game.Overlays
new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = currentChannelContainer = new Container<DrawableChannel>
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = currentChannelContainer = new Container<DrawableChannel>
{
RelativeSizeAxes = Axes.Both,
}
}
},
loading = new LoadingLayer(true),
@@ -162,7 +162,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
{
base.LoadComplete();
apiFriends.BindTo(api.Friends);
apiFriends.BindTo(api.LocalUserState.Friends);
apiFriends.BindCollectionChanged((_, _) => reloadList());
userListToolbar.DisplayStyle.BindValueChanged(_ => reloadList(), true);
@@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
{
base.LoadComplete();
apiFriends.BindTo(api.Friends);
apiFriends.BindTo(api.LocalUserState.Friends);
apiFriends.BindCollectionChanged((_, _) => updateCounts());
friendPresences.BindTo(metadataClient.FriendPresences);
+44 -9
View File
@@ -3,8 +3,10 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Layout;
using osuTK;
namespace osu.Game.Overlays
@@ -21,7 +23,7 @@ namespace osu.Game.Overlays
set
{
allowScrolling = value;
ScheduleAfterChildren(updateScrolling);
scrollCached.Invalidate();
}
}
@@ -49,15 +51,27 @@ namespace osu.Game.Overlays
private Func<Drawable>? createContent;
public new MarginPadding Padding
{
get => base.Padding;
set => base.Padding = value;
}
public float OverflowSpacing { get; init; } = 15;
private const float pixels_per_second = 50;
private const float padding = 15;
private Drawable mainContent = null!;
private Drawable fillerContent = null!;
private FillFlowContainer flow = null!;
private readonly Cached scrollCached = new Cached();
private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize);
public MarqueeContainer()
{
AddLayout(drawSizeLayout);
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
}
@@ -65,14 +79,14 @@ namespace osu.Game.Overlays
[BackgroundDependencyLoader]
private void load()
{
InternalChild = flow = new FillFlowContainer
InternalChild = flow = new MarqueeFlow
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Anchor = NonOverflowingContentAnchor,
Origin = NonOverflowingContentAnchor,
Spacing = new Vector2(padding),
Padding = new MarginPadding { Horizontal = padding },
Spacing = new Vector2(OverflowSpacing),
OnRequiredParentSizeInvalidated = () => scrollCached.Invalidate(),
};
}
@@ -92,12 +106,17 @@ namespace osu.Game.Overlays
flow.Add(mainContent = createContent());
flow.Add(fillerContent = createContent().With(d => d.Alpha = 0));
ScheduleAfterChildren(updateScrolling);
scrollCached.Invalidate();
}
private void updateScrolling()
protected override void UpdateAfterChildren()
{
float overflowWidth = mainContent.DrawWidth + padding - DrawWidth;
base.UpdateAfterChildren();
if (scrollCached.IsValid && drawSizeLayout.IsValid)
return;
float overflowWidth = mainContent.DrawWidth - DrawWidth;
if (overflowWidth > 0 && AllowScrolling)
{
@@ -105,7 +124,7 @@ namespace osu.Game.Overlays
flow.Anchor = Anchor.TopLeft;
flow.Origin = Anchor.TopLeft;
float targetX = mainContent.DrawWidth + padding;
float targetX = mainContent.DrawWidth + OverflowSpacing;
flow.MoveToX(0)
.Delay(InitialMoveDelay)
@@ -120,6 +139,22 @@ namespace osu.Game.Overlays
flow.Anchor = NonOverflowingContentAnchor;
flow.Origin = NonOverflowingContentAnchor;
}
scrollCached.Validate();
drawSizeLayout.Validate();
}
private partial class MarqueeFlow : FillFlowContainer
{
public required Action OnRequiredParentSizeInvalidated { get; init; }
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
{
if (invalidation.HasFlag(Invalidation.RequiredParentSizeToFit))
OnRequiredParentSizeInvalidated.Invoke();
return base.OnInvalidate(invalidation, source);
}
}
}
}
+1
View File
@@ -49,6 +49,7 @@ namespace osu.Game.Overlays.Music
RelativeSizeAxes = Axes.X,
InitialMoveDelay = 0,
AllowScrolling = false,
Padding = new MarginPadding { Horizontal = 15 },
};
selectedSet.BindTo(playlistOverlay.SelectedSet);
+2
View File
@@ -121,6 +121,7 @@ namespace osu.Game.Overlays
Origin = Anchor.Centre,
},
NonOverflowingContentAnchor = Anchor.Centre,
Padding = new MarginPadding { Horizontal = 15 },
},
artist = new MarqueeContainer
{
@@ -136,6 +137,7 @@ namespace osu.Game.Overlays
Origin = Anchor.Centre,
},
NonOverflowingContentAnchor = Anchor.Centre,
Padding = new MarginPadding { Horizontal = 15 },
},
new Container
{
@@ -157,7 +157,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
}
dailyPlayCount.Text = DailyChallengeStatsDisplayStrings.UnitDay(stats.PlayCount.ToLocalisableString("N0"));
dailyPlayCount.Colour = colours.ForRankingTier(DailyChallengeStatsTooltip.TierForPlayCount(stats.PlayCount));
dailyPlayCount.Colour = OsuColour.ForRankingTier(DailyChallengeStatsTooltip.TierForPlayCount(stats.PlayCount));
bool playedToday = stats.LastUpdate?.Date == DateTimeOffset.UtcNow.Date;
bool userIsOnOwnProfile = stats.UserID == api.LocalUser.Value.Id;
@@ -36,9 +36,6 @@ namespace osu.Game.Overlays.Profile.Header.Components
private Box topBackground = null!;
private Box background = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
@@ -117,19 +114,19 @@ namespace osu.Game.Overlays.Profile.Header.Components
topBackground.Colour = colourProvider.Background5;
totalParticipation.Value = DailyChallengeStatsDisplayStrings.UnitDay(statistics.PlayCount.ToLocalisableString(@"N0"));
totalParticipation.ValueColour = colours.ForRankingTier(TierForPlayCount(statistics.PlayCount));
totalParticipation.ValueColour = OsuColour.ForRankingTier(TierForPlayCount(statistics.PlayCount));
currentDaily.Value = DailyChallengeStatsDisplayStrings.UnitDay(content.Statistics.DailyStreakCurrent.ToLocalisableString(@"N0"));
currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent));
currentDaily.ValueColour = OsuColour.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent));
currentWeekly.Value = DailyChallengeStatsDisplayStrings.UnitWeek(statistics.WeeklyStreakCurrent.ToLocalisableString(@"N0"));
currentWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakCurrent));
currentWeekly.ValueColour = OsuColour.ForRankingTier(TierForWeekly(statistics.WeeklyStreakCurrent));
bestDaily.Value = DailyChallengeStatsDisplayStrings.UnitDay(statistics.DailyStreakBest.ToLocalisableString(@"N0"));
bestDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakBest));
bestDaily.ValueColour = OsuColour.ForRankingTier(TierForDaily(statistics.DailyStreakBest));
bestWeekly.Value = DailyChallengeStatsDisplayStrings.UnitWeek(statistics.WeeklyStreakBest.ToLocalisableString(@"N0"));
bestWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakBest));
bestWeekly.ValueColour = OsuColour.ForRankingTier(TierForWeekly(statistics.WeeklyStreakBest));
topTen.Value = statistics.Top10PercentPlacements.ToLocalisableString(@"N0");
topTen.ValueColour = colourProvider.Content2;
@@ -101,7 +101,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
status.Value = FriendStatus.None;
}
api.UpdateLocalFriends();
api.LocalUserState.UpdateFriends();
HideLoadingLayer();
};
@@ -124,7 +124,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
base.LoadComplete();
apiFriends.BindTo(api.Friends);
apiFriends.BindTo(api.LocalUserState.Friends);
apiFriends.BindCollectionChanged((_, _) => Schedule(updateStatus));
User.BindValueChanged(u =>
@@ -27,9 +27,6 @@ namespace osu.Game.Overlays.Profile.Header.Components
private OsuSpriteText levelText = null!;
private Sprite sprite = null!;
[Resolved]
private OsuColour osuColour { get; set; } = null!;
public LevelBadge()
{
TooltipText = UsersStrings.ShowStatsLevel("0");
@@ -91,7 +88,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
tier = RankingTier.Lustrous;
}
return osuColour.ForRankingTier(tier);
return OsuColour.ForRankingTier(tier);
}
}
}
@@ -97,7 +97,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
Background.Colour = colourProvider.Background6;
bool userBlocked = api.Blocks.Any(b => b.TargetID == user.Id);
bool userBlocked = api.LocalUserState.Blocks.Any(b => b.TargetID == user.Id);
AllowableAnchors = [Anchor.BottomCentre, Anchor.TopCentre];
@@ -1,13 +1,13 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using System.Collections.Generic;
using System.Linq;
using osu.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
@@ -19,9 +19,11 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
protected override LocalisableString Header => AudioSettingsStrings.AudioDevicesHeader;
[Resolved]
private AudioManager audio { get; set; }
private AudioManager audio { get; set; } = null!;
private SettingsDropdown<string> dropdown;
private SettingsDropdown<string> dropdown = null!;
private SettingsCheckbox? wasapiExperimental;
[BackgroundDependencyLoader]
private void load()
@@ -32,17 +34,44 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{
LabelText = AudioSettingsStrings.OutputDevice,
Keywords = new[] { "speaker", "headphone", "output" }
}
},
};
updateItems();
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
{
Add(wasapiExperimental = new SettingsCheckbox
{
LabelText = "Use experimental audio mode",
TooltipText = "This will attempt to initialise the audio engine in a lower latency mode.",
Current = audio.UseExperimentalWasapi,
Keywords = new[] { "wasapi", "latency", "exclusive" }
});
wasapiExperimental.Current.ValueChanged += _ => onDeviceChanged(string.Empty);
}
audio.OnNewDevice += onDeviceChanged;
audio.OnLostDevice += onDeviceChanged;
dropdown.Current = audio.AudioDevice;
onDeviceChanged(string.Empty);
}
private void onDeviceChanged(string name) => updateItems();
private void onDeviceChanged(string _)
{
updateItems();
if (wasapiExperimental != null)
{
if (wasapiExperimental.Current.Value)
{
wasapiExperimental.SetNoticeText(
"Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value.", true);
}
else
wasapiExperimental.ClearNoticeText();
}
}
private void updateItems()
{
@@ -61,7 +90,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
// functionality would require involved OS-specific code.
dropdown.Items = deviceItems
// Dropdown doesn't like null items. Somehow we are seeing some arrive here (see https://github.com/ppy/osu/issues/21271)
.Where(i => i != null)
.Where(i => i.IsNotNull())
.Distinct()
.ToList();
}
@@ -70,7 +99,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{
base.Dispose(isDisposing);
if (audio != null)
if (audio.IsNotNull())
{
audio.OnNewDevice -= onDeviceChanged;
audio.OnLostDevice -= onDeviceChanged;
@@ -169,7 +169,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
else
{
applySuggestion.Enabled.Value = true;
hintText.Text = AudioSettingsStrings.SuggestedOffsetValueReceived(averageHitErrorHistory.Count, SuggestedOffset.Value.Value.ToStandardFormattedString(0, false));
hintText.Text = AudioSettingsStrings.SuggestedOffsetValueReceived(averageHitErrorHistory.Count, SuggestedOffset.Value.Value.ToStandardFormattedString(0));
}
}
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -11,6 +12,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osuTK;
namespace osu.Game.Overlays.Settings
@@ -94,7 +96,7 @@ namespace osu.Game.Overlays.Settings
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular),
Text = @"back",
Text = CommonStrings.Back.ToLower(),
},
}
}
+5 -3
View File
@@ -3,12 +3,14 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@@ -21,7 +23,7 @@ namespace osu.Game.Overlays
{
public partial class SettingsToolboxGroup : Container, IExpandable
{
private readonly string title;
private readonly LocalisableString title;
public const int CONTAINER_WIDTH = 270;
private const float transition_duration = 250;
@@ -60,7 +62,7 @@ namespace osu.Game.Overlays
/// Create a new instance.
/// </summary>
/// <param name="title">The title to be displayed in the header of this group.</param>
public SettingsToolboxGroup(string title)
public SettingsToolboxGroup(LocalisableString title)
{
this.title = title;
@@ -102,7 +104,7 @@ namespace osu.Game.Overlays
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Text = title.ToUpperInvariant(),
Text = title.ToUpper(),
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17),
Padding = new MarginPadding { Left = 10, Right = 30 },
},
@@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mods
};
[SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
public DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable
public virtual DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 0,
@@ -608,7 +608,16 @@ namespace osu.Game.Rulesets.Objects.Legacy
public class LegacyHitSampleInfo : HitSampleInfo, IEquatable<LegacyHitSampleInfo>
{
public readonly int CustomSampleBank;
public int CustomSampleBank
{
get
{
if (Suffix != null)
return int.Parse(Suffix);
return UseBeatmapSamples ? 1 : 0;
}
}
/// <summary>
/// Whether this hit sample is layered.
@@ -626,16 +635,33 @@ namespace osu.Game.Rulesets.Objects.Legacy
public bool BankSpecified;
public LegacyHitSampleInfo(string name, string? bank = null, int volume = 0, bool editorAutoBank = false, int customSampleBank = 0, bool isLayered = false)
: base(name, bank ?? SampleControlPoint.DEFAULT_BANK, customSampleBank >= 2 ? customSampleBank.ToString() : null, volume, editorAutoBank)
: base(
name,
bank ?? SampleControlPoint.DEFAULT_BANK,
suffix: customSampleBank >= 2 ? customSampleBank.ToString() : null,
volume,
editorAutoBank,
useBeatmapSamples: customSampleBank >= 1)
{
CustomSampleBank = customSampleBank;
BankSpecified = !string.IsNullOrEmpty(bank);
IsLayered = isLayered;
}
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default,
Optional<bool> newEditorAutoBank = default)
=> With(newName, newBank, newVolume, newEditorAutoBank);
Optional<bool> newEditorAutoBank = default, Optional<bool> newUseBeatmapSamples = default)
{
string? suffix = newSuffix.GetOr(Suffix);
bool useBeatmapSamples = newUseBeatmapSamples.GetOr(UseBeatmapSamples);
int newCustomSampleBank = 0;
if (suffix != null)
_ = int.TryParse(suffix, out newCustomSampleBank);
if (newCustomSampleBank == 0 && useBeatmapSamples)
newCustomSampleBank = 1;
return With(newName, newBank, newVolume, newEditorAutoBank, newCustomSampleBank);
}
public virtual LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default,
Optional<bool> newEditorAutoBank = default, Optional<int> newCustomSampleBank = default, Optional<bool> newIsLayered = default)
@@ -157,6 +157,8 @@ namespace osu.Game.Rulesets.UI
public IBindable<double> AggregateTempo => throw new NotSupportedException();
public void Invalidate(string name) => throw new NotSupportedException();
public int PlaybackConcurrency
{
get => throw new NotSupportedException();

Some files were not shown because too many files have changed in this diff Show More