mirror of
https://github.com/SK-la/Ez2Lazer.git
synced 2026-03-15 03:20:27 +00:00
同步更新
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -203,8 +203,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
|
||||
LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
|
||||
|
||||
var objects = new List<DifficultyHitObject>();
|
||||
var perColumnObjects = new List<DifficultyHitObject>[totalColumns];
|
||||
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
||||
List<DifficultyHitObject>[] perColumnObjects = new List<DifficultyHitObject>[totalColumns];
|
||||
|
||||
for (int column = 0; column < totalColumns; column++)
|
||||
perColumnObjects[column] = new List<DifficultyHitObject>();
|
||||
@@ -220,18 +220,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
}
|
||||
|
||||
// Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
|
||||
protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input;
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
{
|
||||
return new Skill[]
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
|
||||
{
|
||||
new Strain(mods, ((ManiaBeatmap)Beatmap).TotalColumns)
|
||||
};
|
||||
}
|
||||
|
||||
protected override Mod[] DifficultyAdjustmentMods
|
||||
{
|
||||
@@ -242,7 +236,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
new ManiaModDoubleTime(),
|
||||
new ManiaModHalfTime(),
|
||||
new ManiaModEasy(),
|
||||
new ManiaModHardRock()
|
||||
new ManiaModHardRock(),
|
||||
};
|
||||
|
||||
if (isForCurrentRuleset)
|
||||
@@ -264,7 +258,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
new ManiaModKey8(),
|
||||
new MultiMod(new ManiaModKey8(), new ManiaModDualStages()),
|
||||
new ManiaModKey9(),
|
||||
new MultiMod(new ManiaModKey9(), new ManiaModDualStages())
|
||||
new MultiMod(new ManiaModKey9(), new ManiaModDualStages()),
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Backgrounds;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -79,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);
|
||||
}
|
||||
|
||||
@@ -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)" },
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -124,11 +124,11 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
|
||||
foreach (var user in MultiplayerClient.ServerRoom!.Users.OrderBy(_ => RNG.Next()))
|
||||
{
|
||||
state.Users[user.UserID].Placement = i++;
|
||||
state.Users[user.UserID].Points = (8 - i) * 7;
|
||||
state.Users[user.UserID].Rounds[1].Placement = 1;
|
||||
state.Users[user.UserID].Rounds[1].TotalScore = 1;
|
||||
state.Users[user.UserID].Rounds[1].Statistics[HitResult.LargeBonus] = 1;
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -36,28 +36,28 @@ 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.GetOrAdd(localUserId).Placement = 1;
|
||||
state.Users.GetOrAdd(localUserId).Points = 8;
|
||||
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.GetOrAdd(localUserId).Rounds.GetOrAdd(1).TotalScore = 1000;
|
||||
|
||||
// Highest accuracy.
|
||||
state.Users[localUserId].Rounds[2].Accuracy = 0.9995;
|
||||
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(2).Accuracy = 0.9995;
|
||||
|
||||
// Highest combo.
|
||||
state.Users[localUserId].Rounds[3].MaxCombo = 100;
|
||||
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(3).MaxCombo = 100;
|
||||
|
||||
// Most bonus score.
|
||||
state.Users[localUserId].Rounds[4].Statistics[HitResult.LargeBonus] = 50;
|
||||
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(4).Statistics[HitResult.LargeBonus] = 50;
|
||||
|
||||
// Smallest score difference.
|
||||
state.Users[localUserId].Rounds[5].TotalScore = 1000;
|
||||
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(5).TotalScore = 1000;
|
||||
|
||||
// Largest score difference.
|
||||
state.Users[localUserId].Rounds[6].TotalScore = 1000;
|
||||
state.Users.GetOrAdd(localUserId).Rounds.GetOrAdd(6).TotalScore = 1000;
|
||||
|
||||
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
|
||||
});
|
||||
@@ -103,36 +103,78 @@ 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();
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserWithNoScore()
|
||||
{
|
||||
AddStep("join another user", () => MultiplayerClient.AddUser(new MultiplayerRoomUser(2)
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "Other user"
|
||||
}
|
||||
}));
|
||||
|
||||
AddStep("show results with no score", () =>
|
||||
{
|
||||
var state = new MatchmakingRoomState
|
||||
{
|
||||
CurrentRound = 6,
|
||||
Stage = MatchmakingStage.Ended
|
||||
};
|
||||
|
||||
state.Users.GetOrAdd(API.LocalUser.Value.OnlineID).Rounds.GetOrAdd(1).Placement = 1;
|
||||
state.Users.GetOrAdd(2);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,8 +472,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
{
|
||||
AddUntilStep($"config value is {configValue}", () => getConfigManager().Get<double>(ManiaRulesetSetting.ScrollSpeed), () => Is.EqualTo(configValue));
|
||||
AddUntilStep($"gameplay value is {gameplayValue}", () => this.ChildrenOfType<DrawableManiaRuleset>().Single().TargetTimeRange,
|
||||
() => Is.EqualTo(DrawableManiaRuleset.ComputeScrollTime(gameplayValue, 200, 1)));
|
||||
// 按自定义的方式计算
|
||||
() => Is.EqualTo(DrawableManiaRuleset.ComputeScrollTime(gameplayValue, 1,1)));
|
||||
}
|
||||
|
||||
ManiaRulesetConfigManager getConfigManager() => ((ManiaRulesetConfigManager)Game.Dependencies.Get<IRulesetConfigCache>().GetConfigFor(new ManiaRuleset())!);
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
// 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;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@@ -13,25 +16,35 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneDrawableDate : OsuTestScene
|
||||
{
|
||||
public TestSceneDrawableDate()
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
AddStep("Create 7 dates", () =>
|
||||
{
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(60))),
|
||||
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(55))),
|
||||
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(50))),
|
||||
new PokeyDrawableDate(DateTimeOffset.Now),
|
||||
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(60))),
|
||||
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(65))),
|
||||
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(70))),
|
||||
}
|
||||
};
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(60))),
|
||||
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(55))),
|
||||
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(50))),
|
||||
new PokeyDrawableDate(DateTimeOffset.Now),
|
||||
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(60))),
|
||||
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(65))),
|
||||
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(70))),
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSecondsUpdate()
|
||||
{
|
||||
AddUntilStep("4th date says \"2 seconds ago\"", () => this.ChildrenOfType<DrawableDate>().ElementAt(3).Current.Value == "2 seconds ago");
|
||||
}
|
||||
|
||||
private partial class PokeyDrawableDate : CompositeDrawable
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,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>
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -34,8 +34,6 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public Storyboard Storyboard => storyboard.Value;
|
||||
|
||||
public IBeatmap BeatmapAfterConverted;
|
||||
|
||||
public ISkin Skin => skin.Value;
|
||||
|
||||
private AudioManager audioManager { get; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Utils;
|
||||
@@ -80,7 +81,7 @@ namespace osu.Game.Graphics
|
||||
|
||||
public DateTimeOffset TooltipContent => Date;
|
||||
|
||||
private class HumanisedDate : IEquatable<HumanisedDate>, ILocalisableStringData
|
||||
private class HumanisedDate : ILocalisableStringData
|
||||
{
|
||||
public readonly DateTimeOffset Date;
|
||||
|
||||
@@ -89,11 +90,18 @@ namespace osu.Game.Graphics
|
||||
Date = date;
|
||||
}
|
||||
|
||||
public bool Equals(HumanisedDate? other)
|
||||
=> other?.Date != null && Date.Equals(other.Date);
|
||||
|
||||
public bool Equals(ILocalisableStringData? other)
|
||||
=> other is HumanisedDate otherDate && Equals(otherDate);
|
||||
/// <remarks>
|
||||
/// Humanizer formats the <see cref="Date"/> relative to the local computer time.
|
||||
/// Therefore, replacing a <see cref="HumanisedDate"/> instance with another instance of the class with the same <see cref="Date"/>
|
||||
/// should have the effect of replacing and re-formatting the text.
|
||||
/// Including <see cref="Date"/> in equality members would stop this from happening, as <see cref="SpriteText.Text"/>
|
||||
/// has equality-based early guards to prevent redundant text replaces.
|
||||
/// Thus, instances of these class just compare <see langword="false"/> to any <see cref="ILocalisableStringData"/> to ensure re-formatting happens correctly.
|
||||
/// There are "technically" more "correct" ways to do this (like also including the current time into equality checks),
|
||||
/// but they are simultaneously functionally equivalent to this and overly convoluted.
|
||||
/// This is a private hack-job of a wrapper around humanizer anyway.
|
||||
/// </remarks>
|
||||
public bool Equals(ILocalisableStringData? other) => false;
|
||||
|
||||
public string GetLocalised(LocalisationParameters parameters) => HumanizerUtils.Humanize(Date);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
osu.Game/Online/API/ILocalUserState.cs
Normal file
20
osu.Game/Online/API/ILocalUserState.cs
Normal 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
osu.Game/Online/API/LocalUserState.cs
Normal file
151
osu.Game/Online/API/LocalUserState.cs
Normal 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; } = [];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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))));
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -162,16 +162,17 @@ namespace osu.Game.Overlays
|
||||
private int runningDepth;
|
||||
|
||||
private readonly Scheduler postScheduler = new Scheduler();
|
||||
private readonly Scheduler criticalPostScheduler = new Scheduler();
|
||||
|
||||
public override bool IsPresent =>
|
||||
// Delegate presence as we need to consider the toast tray in addition to the main overlay.
|
||||
State.Value == Visibility.Visible || mainContent.IsPresent || toastTray.IsPresent || postScheduler.HasPendingTasks;
|
||||
State.Value == Visibility.Visible || mainContent.IsPresent || toastTray.IsPresent || postScheduler.HasPendingTasks || criticalPostScheduler.HasPendingTasks;
|
||||
|
||||
private bool processingPosts = true;
|
||||
|
||||
private double? lastSamplePlayback;
|
||||
|
||||
public void Post(Notification notification) => postScheduler.Add(() =>
|
||||
public void Post(Notification notification) => (notification.IsCritical ? criticalPostScheduler : postScheduler).Add(() =>
|
||||
{
|
||||
++runningDepth;
|
||||
|
||||
@@ -220,6 +221,8 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
base.Update();
|
||||
|
||||
criticalPostScheduler.Update();
|
||||
|
||||
if (processingPosts)
|
||||
postScheduler.Update();
|
||||
}
|
||||
|
||||
@@ -91,7 +91,12 @@ namespace osu.Game.Overlays
|
||||
public void FlushAllToasts()
|
||||
{
|
||||
foreach (var notification in toastFlow.ToArray())
|
||||
{
|
||||
if (notification.IsCritical)
|
||||
continue;
|
||||
|
||||
forwardNotification(notification);
|
||||
}
|
||||
}
|
||||
|
||||
public void Post(Notification notification)
|
||||
|
||||
@@ -39,6 +39,11 @@ namespace osu.Game.Overlays.Notifications
|
||||
/// </summary>
|
||||
public bool IsImportant { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Critical notifications show even during gameplay or other scenarios where notifications would usually be suppressed.
|
||||
/// </summary>
|
||||
public bool IsCritical { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Transient notifications only show as a toast, and do not linger in notification history.
|
||||
/// </summary>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins);
|
||||
config.BindWith(OsuSetting.HUDVisibilityMode, configVisibilityMode);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -117,7 +118,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
globallyDisableBeatmapSkinSetting();
|
||||
overrideSkinEditorRelevantSettings();
|
||||
|
||||
if (skinEditor != null)
|
||||
{
|
||||
@@ -159,7 +160,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
nestedInputManagerDisable?.Dispose();
|
||||
nestedInputManagerDisable = null;
|
||||
|
||||
globallyReenableBeatmapSkinSetting();
|
||||
restoreSkinEditorRelevantSettings();
|
||||
}
|
||||
|
||||
public void PresentGameplay() => presentGameplay(false);
|
||||
@@ -330,24 +331,33 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
private readonly Bindable<bool> beatmapSkins = new Bindable<bool>();
|
||||
private LeasedBindable<bool>? leasedBeatmapSkins;
|
||||
|
||||
private void globallyDisableBeatmapSkinSetting()
|
||||
{
|
||||
if (beatmapSkins.Disabled)
|
||||
return;
|
||||
private readonly Bindable<HUDVisibilityMode> configVisibilityMode = new Bindable<HUDVisibilityMode>();
|
||||
private LeasedBindable<HUDVisibilityMode>? leasedVisibilityMode;
|
||||
|
||||
// The skin editor doesn't work well if beatmap skins are being applied to the player screen.
|
||||
// To keep things simple, disable the setting game-wide while using the skin editor.
|
||||
//
|
||||
// This causes a full reload of the skin, which is pretty ugly.
|
||||
// TODO: Investigate if we can avoid this when a beatmap skin is not being applied by the current beatmap.
|
||||
leasedBeatmapSkins = beatmapSkins.BeginLease(true);
|
||||
leasedBeatmapSkins.Value = false;
|
||||
private void overrideSkinEditorRelevantSettings()
|
||||
{
|
||||
if (!beatmapSkins.Disabled)
|
||||
{
|
||||
// The skin editor doesn't work well if beatmap skins are being applied to the player screen.
|
||||
// To keep things simple, disable the setting game-wide while using the skin editor.
|
||||
//
|
||||
// This causes a full reload of the skin, which is pretty ugly.
|
||||
// TODO: Investigate if we can avoid this when a beatmap skin is not being applied by the current beatmap.
|
||||
leasedBeatmapSkins = beatmapSkins.BeginLease(true);
|
||||
leasedBeatmapSkins.Value = false;
|
||||
}
|
||||
|
||||
leasedVisibilityMode = configVisibilityMode.BeginLease(true);
|
||||
leasedVisibilityMode.Value = HUDVisibilityMode.Always;
|
||||
}
|
||||
|
||||
private void globallyReenableBeatmapSkinSetting()
|
||||
private void restoreSkinEditorRelevantSettings()
|
||||
{
|
||||
leasedBeatmapSkins?.Return();
|
||||
leasedBeatmapSkins = null;
|
||||
|
||||
leasedVisibilityMode?.Return();
|
||||
leasedVisibilityMode = null;
|
||||
}
|
||||
|
||||
public new void ToggleVisibility()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osuTK;
|
||||
using System.Reflection;
|
||||
|
||||
namespace osu.Game.Screens.Backgrounds
|
||||
{
|
||||
@@ -105,6 +106,14 @@ namespace osu.Game.Screens.Backgrounds
|
||||
columnBlur.BindValueChanged(e =>
|
||||
{
|
||||
replicatedBackground?.BlurTo(new Vector2((float)e.NewValue * 80f), 0);
|
||||
|
||||
// Also apply blur to storyboard if present
|
||||
if (replicatedBackground is BeatmapBackgroundWithStoryboard bws)
|
||||
{
|
||||
var storyboardContainerField = typeof(BeatmapBackgroundWithStoryboard).GetField("storyboardContainer", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
// if (storyboardContainerField?.GetValue(bws) is Container storyboardContainer)
|
||||
// storyboardContainer.BlurSigma = new Vector2((float)e.NewValue * 80f);
|
||||
}
|
||||
}, true);
|
||||
|
||||
columnWidth.BindValueChanged(e => updateWidth(), true);
|
||||
@@ -125,7 +134,7 @@ namespace osu.Game.Screens.Backgrounds
|
||||
for (int i = 0; i < keyMode; i++)
|
||||
totalWidth += getColumnWidth(keyMode, i);
|
||||
|
||||
float totalScale = uiScale.Value / 0.938f;
|
||||
float totalScale = uiScale.Value / 0.936f;
|
||||
maskingContainer.Width = totalWidth / totalScale;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace osu.Game.Screens.Footer
|
||||
|
||||
private Box background = null!;
|
||||
private FillFlowContainer<ScreenFooterButton> buttonsFlow = null!;
|
||||
private Container footerContentContainer = null!;
|
||||
private Container overlayContentContainer = null!;
|
||||
private Container<ScreenFooterButton> hiddenButtonsContainer = null!;
|
||||
|
||||
private LogoTrackingContainer logoTrackingContainer = null!;
|
||||
@@ -102,6 +102,7 @@ namespace osu.Game.Screens.Footer
|
||||
{
|
||||
buttonsFlow = new FillFlowContainer<ScreenFooterButton>
|
||||
{
|
||||
Name = "Visible footer buttons",
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Y = ScreenFooterButton.CORNER_RADIUS,
|
||||
@@ -109,8 +110,9 @@ namespace osu.Game.Screens.Footer
|
||||
Spacing = new Vector2(7, 0),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
},
|
||||
footerContentContainer = new Container
|
||||
overlayContentContainer = new Container
|
||||
{
|
||||
Name = "Overlay-provided extra content",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Y = -OsuGame.SCREEN_EDGE_MARGIN,
|
||||
},
|
||||
@@ -126,6 +128,7 @@ namespace osu.Game.Screens.Footer
|
||||
},
|
||||
hiddenButtonsContainer = new Container<ScreenFooterButton>
|
||||
{
|
||||
Name = "Hidden footer buttons",
|
||||
Margin = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding },
|
||||
Y = ScreenFooterButton.CORNER_RADIUS,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
@@ -234,11 +237,11 @@ namespace osu.Game.Screens.Footer
|
||||
|
||||
public ShearedOverlayContainer? ActiveOverlay { get; private set; }
|
||||
|
||||
private VisibilityContainer? activeFooterContent;
|
||||
private VisibilityContainer? activeOverlayContent;
|
||||
|
||||
private readonly List<ScreenFooterButton> temporarilyHiddenButtons = new List<ScreenFooterButton>();
|
||||
|
||||
public IDisposable RegisterActiveOverlayContainer(ShearedOverlayContainer overlay, out VisibilityContainer? footerContent)
|
||||
public IDisposable RegisterActiveOverlayContainer(ShearedOverlayContainer overlay, out VisibilityContainer? overlayContent)
|
||||
{
|
||||
if (ActiveOverlay != null)
|
||||
{
|
||||
@@ -267,12 +270,12 @@ namespace osu.Game.Screens.Footer
|
||||
|
||||
updateColourScheme(overlay.ColourProvider.Hue);
|
||||
|
||||
footerContent = overlay.CreateFooterContent();
|
||||
activeFooterContent = footerContent;
|
||||
var content = footerContent;
|
||||
overlayContent = overlay.CreateFooterContent();
|
||||
activeOverlayContent = overlayContent;
|
||||
var content = overlayContent;
|
||||
|
||||
if (content != null)
|
||||
footerContentContainer.Child = content;
|
||||
overlayContentContainer.Child = content;
|
||||
|
||||
if (temporarilyHiddenButtons.Count > 0)
|
||||
this.Delay(60).Schedule(() => content?.Show());
|
||||
@@ -287,15 +290,19 @@ namespace osu.Game.Screens.Footer
|
||||
if (ActiveOverlay == null)
|
||||
return;
|
||||
|
||||
Debug.Assert(activeFooterContent != null);
|
||||
activeFooterContent.Hide();
|
||||
Debug.Assert(activeOverlayContent != null);
|
||||
activeOverlayContent.Hide();
|
||||
|
||||
double timeUntilRun = activeFooterContent.LatestTransformEndTime - Time.Current;
|
||||
double timeUntilRun = activeOverlayContent.LatestTransformEndTime - Time.Current;
|
||||
|
||||
for (int i = 0; i < temporarilyHiddenButtons.Count; i++)
|
||||
{
|
||||
var button = temporarilyHiddenButtons[i];
|
||||
hiddenButtonsContainer.Remove(button, false);
|
||||
// temporarily bypass autosize on the X axis to prevent the buttons taking space
|
||||
// immediately upon being moved back to the flow.
|
||||
// this prevents the overlay content jumping to the right during its fade-out.
|
||||
button.BypassAutoSizeAxes = Axes.X;
|
||||
buttonsFlow.Add(button);
|
||||
|
||||
makeButtonAppearFromBottom(button, 0);
|
||||
@@ -305,8 +312,13 @@ namespace osu.Game.Screens.Footer
|
||||
|
||||
updateColourScheme(OverlayColourScheme.Aquamarine.GetHue());
|
||||
|
||||
activeFooterContent.Delay(timeUntilRun).Expire();
|
||||
activeFooterContent = null;
|
||||
activeOverlayContent.Delay(timeUntilRun).Schedule(() =>
|
||||
{
|
||||
// overlay content is done displaying, re-enable autosize on all active buttons
|
||||
foreach (var button in buttonsFlow)
|
||||
button.BypassAutoSizeAxes = Axes.None;
|
||||
}).Expire();
|
||||
activeOverlayContent = null;
|
||||
ActiveOverlay = null;
|
||||
}
|
||||
|
||||
|
||||
220
osu.Game/Screens/Footer/ScreenStackFooter.cs
Normal file
220
osu.Game/Screens/Footer/ScreenStackFooter.cs
Normal file
@@ -0,0 +1,220 @@
|
||||
// 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 osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Screens.Footer
|
||||
{
|
||||
public partial class ScreenStackFooter : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when logo tracking begins, intended to bring the osu! logo to the frontmost visually.
|
||||
/// </summary>
|
||||
public Action<bool>? RequestLogoInFront { private get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The back button was pressed.
|
||||
/// </summary>
|
||||
public Action? BackButtonPressed { private get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The (legacy) back button.
|
||||
/// </summary>
|
||||
public readonly BackButton BackButton;
|
||||
|
||||
/// <summary>
|
||||
/// The footer.
|
||||
/// </summary>
|
||||
public readonly ScreenFooter Footer;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the legacy back button is currently displayed.
|
||||
/// </summary>
|
||||
private readonly IBindable<bool> backButtonVisibility = new BindableBool();
|
||||
|
||||
private readonly ScreenStackTracker screenTracker;
|
||||
|
||||
public ScreenStackFooter(ScreenStack screenStack, ScreenFooter.BackReceptor? backReceptor = null)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
BackButton = new BackButton(backReceptor)
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Action = () => BackButtonPressed?.Invoke(),
|
||||
},
|
||||
Footer = new ScreenFooter(backReceptor)
|
||||
{
|
||||
RequestLogoInFront = v => RequestLogoInFront?.Invoke(v),
|
||||
BackButtonPressed = () => BackButtonPressed?.Invoke()
|
||||
}
|
||||
};
|
||||
|
||||
screenTracker = new ScreenStackTracker(screenStack);
|
||||
screenTracker.ScreenChanged += onScreenChanged;
|
||||
|
||||
backButtonVisibility.ValueChanged += onBackButtonVisibilityChanged;
|
||||
}
|
||||
|
||||
private void onScreenChanged(IScreen lastScreen, IScreen newScreen)
|
||||
{
|
||||
unbindScreen(lastScreen);
|
||||
bindScreen(newScreen);
|
||||
}
|
||||
|
||||
private void onBackButtonVisibilityChanged(ValueChangedEvent<bool> visible)
|
||||
{
|
||||
if (visible.NewValue)
|
||||
BackButton.Show();
|
||||
else
|
||||
BackButton.Hide();
|
||||
}
|
||||
|
||||
private void unbindScreen(IScreen screen)
|
||||
{
|
||||
if (screen is not OsuScreen osuScreen)
|
||||
return;
|
||||
|
||||
backButtonVisibility.UnbindFrom(osuScreen.BackButtonVisibility);
|
||||
}
|
||||
|
||||
private void bindScreen(IScreen screen)
|
||||
{
|
||||
if (screen is not OsuScreen osuScreen)
|
||||
{
|
||||
((BindableBool)backButtonVisibility).Value = true;
|
||||
|
||||
Footer.SetButtons([]);
|
||||
Footer.Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (osuScreen.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;
|
||||
|
||||
Footer.Show();
|
||||
|
||||
if (osuScreen.IsLoaded)
|
||||
updateFooterButtons();
|
||||
else
|
||||
{
|
||||
// ensure the current buttons are immediately disabled on screen change (so they can't be pressed).
|
||||
Footer.SetButtons([]);
|
||||
|
||||
osuScreen.OnLoadComplete += _ => updateFooterButtons();
|
||||
}
|
||||
|
||||
void updateFooterButtons()
|
||||
{
|
||||
var buttons = osuScreen.CreateFooterButtons();
|
||||
|
||||
osuScreen.LoadComponentsAgainstScreenDependencies(buttons);
|
||||
|
||||
Footer.SetButtons(buttons);
|
||||
Footer.Show();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
backButtonVisibility.BindTo(osuScreen.BackButtonVisibility);
|
||||
|
||||
Footer.SetButtons([]);
|
||||
Footer.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
screenTracker.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively represents a single screen stack and any nested subscreen stack.
|
||||
/// </summary>
|
||||
private class ScreenStackTracker : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the leading screen changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This differs from <see cref="ScreenStack.ScreenPushed"/> and <see cref="ScreenStack.ScreenExited"/>
|
||||
/// because <c>lastScreen</c> and <c>newScreen</c> may be subscreens of the current screen stack.
|
||||
/// <br />
|
||||
/// As such, no assumptions may be made as to the relation of screens to this entry's <see cref="ScreenStack"/>.
|
||||
/// </remarks>
|
||||
public event ScreenChangedDelegate? ScreenChanged;
|
||||
|
||||
/// <summary>
|
||||
/// The screen stack tracked by this entry.
|
||||
/// </summary>
|
||||
private readonly ScreenStack stack;
|
||||
|
||||
/// <summary>
|
||||
/// An entry corresponding to the subscreen stack of the current screen, if any.
|
||||
/// </summary>
|
||||
private ScreenStackTracker? subScreenTracker;
|
||||
|
||||
/// <summary>
|
||||
/// The screen which should be bound to the screen footer - the most nested subscreen.
|
||||
/// </summary>
|
||||
private IScreen leadingScreen => subScreenTracker?.leadingScreen ?? stack.CurrentScreen;
|
||||
|
||||
public ScreenStackTracker(ScreenStack stack)
|
||||
{
|
||||
this.stack = stack;
|
||||
|
||||
stack.ScreenPushed += onParentScreenChanged;
|
||||
stack.ScreenExited += onParentScreenChanged;
|
||||
}
|
||||
|
||||
private void onParentScreenChanged(IScreen lastScreen, IScreen newScreen)
|
||||
{
|
||||
// The screen which we will be UNBINDING from the screen footer later on.
|
||||
IScreen lastLeadingScreen = subScreenTracker?.leadingScreen ?? lastScreen;
|
||||
|
||||
// Subscreens are attached to a parent screen, so when the parent changes the subscreen must also.
|
||||
subScreenTracker?.Dispose();
|
||||
subScreenTracker = null;
|
||||
|
||||
// Check if we've switched to a screen that has a subscreen.
|
||||
if (newScreen is IHasSubScreenStack newStack)
|
||||
{
|
||||
subScreenTracker = new ScreenStackTracker(newStack.SubScreenStack);
|
||||
subScreenTracker.ScreenChanged += onSubScreenScreenChanged;
|
||||
}
|
||||
|
||||
ScreenChanged?.Invoke(lastLeadingScreen, leadingScreen);
|
||||
}
|
||||
|
||||
private void onSubScreenScreenChanged(IScreen lastScreen, IScreen newScreen)
|
||||
{
|
||||
ScreenChanged?.Invoke(lastScreen, newScreen);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
stack.ScreenPushed -= onParentScreenChanged;
|
||||
stack.ScreenExited -= onParentScreenChanged;
|
||||
|
||||
if (subScreenTracker != null)
|
||||
{
|
||||
subScreenTracker.ScreenChanged -= onSubScreenScreenChanged;
|
||||
subScreenTracker.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
|
||||
if (s.UserID == api.LocalUser.Value.Id)
|
||||
highlightType = BeatmapLeaderboardScore.HighlightType.Own;
|
||||
else if (api.Friends.Any(r => r.TargetID == s.UserID))
|
||||
else if (api.LocalUserState.Friends.Any(r => r.TargetID == s.UserID))
|
||||
highlightType = BeatmapLeaderboardScore.HighlightType.Friend;
|
||||
|
||||
return new BeatmapLeaderboardScore(s, sheared: false)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
@@ -19,6 +20,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 osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
@@ -51,6 +53,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapSetOverlay? beatmapSetOverlay { get; set; }
|
||||
|
||||
public BeatmapCardMatchmaking(APIBeatmap beatmap)
|
||||
: base(beatmap.BeatmapSet!, false)
|
||||
{
|
||||
@@ -315,7 +320,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
get
|
||||
{
|
||||
var items = base.ContextMenuItems.ToList();
|
||||
List<MenuItem> items = new List<MenuItem>
|
||||
{
|
||||
new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, () => beatmapSetOverlay?.FetchAndShowBeatmap(beatmap.OnlineID))
|
||||
};
|
||||
|
||||
foreach (var button in buttonContainer.Buttons)
|
||||
{
|
||||
|
||||
@@ -45,8 +45,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
|
||||
private BeatmapCardMatchmaking? card;
|
||||
|
||||
public override bool PropagatePositionalInputSubTree => AllowSelection;
|
||||
|
||||
public BeatmapSelectPanel(MultiplayerPlaylistItem item)
|
||||
{
|
||||
Item = item;
|
||||
@@ -113,13 +111,27 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
Debug.Assert(card == null);
|
||||
|
||||
var beatmap = b.GetResultSafely()!;
|
||||
APIBeatmap beatmap = b.GetResultSafely() ?? new APIBeatmap
|
||||
{
|
||||
BeatmapSet = new APIBeatmapSet
|
||||
{
|
||||
Title = "unknown beatmap",
|
||||
TitleUnicode = "unknown beatmap",
|
||||
Artist = "unknown artist",
|
||||
ArtistUnicode = "unknown artist",
|
||||
}
|
||||
};
|
||||
|
||||
beatmap.StarRating = Item.StarRating;
|
||||
|
||||
mainContent.Add(card = new BeatmapCardMatchmaking(beatmap)
|
||||
{
|
||||
Depth = float.MaxValue,
|
||||
Action = () => Action?.Invoke(Item),
|
||||
Action = () =>
|
||||
{
|
||||
if (AllowSelection)
|
||||
Action?.Invoke(Item);
|
||||
},
|
||||
});
|
||||
|
||||
foreach (var user in users)
|
||||
@@ -141,11 +153,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
lighting.FadeTo(0.2f, 50)
|
||||
.Then()
|
||||
.FadeTo(0.1f, 300);
|
||||
if (AllowSelection)
|
||||
{
|
||||
lighting.FadeTo(0.2f, 50)
|
||||
.Then()
|
||||
.FadeTo(0.1f, 300);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
@@ -157,11 +173,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
if (e.Button == MouseButton.Left)
|
||||
{
|
||||
if (AllowSelection && e.Button == MouseButton.Left)
|
||||
scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
@@ -171,16 +184,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
base.OnMouseUp(e);
|
||||
|
||||
if (e.Button == MouseButton.Left)
|
||||
{
|
||||
scaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
lighting.FadeTo(0.5f, 50)
|
||||
.Then()
|
||||
.FadeTo(0.1f, 400);
|
||||
if (AllowSelection)
|
||||
{
|
||||
lighting.FadeTo(0.5f, 50)
|
||||
.Then()
|
||||
.FadeTo(0.1f, 400);
|
||||
}
|
||||
|
||||
// pass through to let the beatmap card handle actual click.
|
||||
return false;
|
||||
|
||||
@@ -41,7 +41,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = float.MaxValue,
|
||||
Padding = new MarginPadding(-2),
|
||||
Child = new FastCircle
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@@ -50,20 +49,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
});
|
||||
}
|
||||
|
||||
AddInternal(new CircularContainer
|
||||
AddInternal(new Container
|
||||
{
|
||||
Padding = new MarginPadding(2),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
Child = new CircularContainer
|
||||
{
|
||||
new Box
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.LightSlateGray,
|
||||
},
|
||||
new ClickableAvatar(user, true)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.LightSlateGray,
|
||||
},
|
||||
new ClickableAvatar(user, true)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Transforms;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Input;
|
||||
@@ -66,5 +68,23 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
{
|
||||
}
|
||||
|
||||
public void Appear()
|
||||
{
|
||||
FinishTransforms();
|
||||
|
||||
this.MoveToY(150f)
|
||||
.FadeOut()
|
||||
.MoveToY(0f, 240, Easing.OutCubic)
|
||||
.FadeIn(240, Easing.OutCubic);
|
||||
}
|
||||
|
||||
public TransformSequence<MatchmakingChatDisplay> Disappear()
|
||||
{
|
||||
FinishTransforms();
|
||||
|
||||
return this.FadeOut(240, Easing.InOutCubic)
|
||||
.MoveToY(150f, 240, Easing.InOutCubic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
|
||||
public readonly MultiplayerRoomUser RoomUser;
|
||||
|
||||
/// <summary>
|
||||
/// Perform an action in addition to showing the user's profile.
|
||||
/// This should be used to perform auxiliary tasks and not as a primary action for clicking a user panel (to maintain a consistent UX).
|
||||
/// </summary>
|
||||
public new Action? Action;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
@@ -67,13 +73,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
private IDialogOverlay? dialogOverlay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
protected OverlayColourProvider? ColourProvider { get; private set; }
|
||||
private OverlayColourProvider? colourProvider { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IPerformFromScreenRunner? performer { get; set; }
|
||||
|
||||
[Resolved]
|
||||
protected OsuColour Colours { get; private set; } = null!;
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient? multiplayerClient { get; set; }
|
||||
@@ -81,42 +87,28 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
[Resolved]
|
||||
private MetadataClient? metadataClient { get; set; }
|
||||
|
||||
public readonly APIUser User;
|
||||
private readonly Action viewProfile;
|
||||
|
||||
private OsuSpriteText rankText = null!;
|
||||
private OsuSpriteText scoreText = null!;
|
||||
|
||||
private Drawable avatarPositionTarget = null!;
|
||||
private Drawable avatarJumpTarget = null!;
|
||||
private MatchmakingAvatar avatar = null!;
|
||||
private Drawable avatar = null!;
|
||||
private OsuSpriteText username = null!;
|
||||
|
||||
private Container mainContent = null!;
|
||||
|
||||
private Box solidBackgroundLayer = null!;
|
||||
private Drawable background = null!;
|
||||
|
||||
private OsuSpriteText quitText = null!;
|
||||
private BufferedContainer backgroundQuitTarget = null!;
|
||||
private BufferedContainer avatarQuitTarget = null!;
|
||||
|
||||
private PlayerPanelDisplayMode displayMode = PlayerPanelDisplayMode.Horizontal;
|
||||
|
||||
public PlayerPanelDisplayMode DisplayMode
|
||||
{
|
||||
get => displayMode;
|
||||
set
|
||||
{
|
||||
displayMode = value;
|
||||
if (IsLoaded)
|
||||
updateLayout(false);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly APIUser User;
|
||||
|
||||
/// <summary>
|
||||
/// Perform an action in addition to showing the user's profile.
|
||||
/// This should be used to perform auxiliary tasks and not as a primary action for clicking a user panel (to maintain a consistent UX).
|
||||
/// </summary>
|
||||
public new Action? Action;
|
||||
|
||||
protected Action ViewProfile { get; private set; } = null!;
|
||||
|
||||
public Box SolidBackgroundLayer { get; private set; } = null!;
|
||||
|
||||
protected Drawable? Background { get; private set; }
|
||||
private bool hasQuit;
|
||||
|
||||
public PlayerPanel(MultiplayerRoomUser user)
|
||||
: base(HoverSampleSet.Button)
|
||||
@@ -125,99 +117,126 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
|
||||
User = user.User;
|
||||
RoomUser = user;
|
||||
|
||||
base.Action = viewProfile = () =>
|
||||
{
|
||||
Action?.Invoke();
|
||||
profileOverlay?.ShowUser(User);
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Add(SolidBackgroundLayer = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourProvider?.Background5 ?? Colours.Gray1
|
||||
});
|
||||
|
||||
Background = new UserCoverBackground
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
User = User
|
||||
};
|
||||
if (Background != null)
|
||||
Add(Background);
|
||||
|
||||
base.Action = ViewProfile = () =>
|
||||
{
|
||||
Action?.Invoke();
|
||||
profileOverlay?.ShowUser(User);
|
||||
};
|
||||
|
||||
Content.Masking = true;
|
||||
Content.CornerRadius = 10;
|
||||
Content.CornerExponent = 10;
|
||||
Content.Anchor = Anchor.Centre;
|
||||
Content.Origin = Anchor.Centre;
|
||||
|
||||
Add(new Container
|
||||
Child = backgroundQuitTarget = new BufferedContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
FrameBufferScale = new Vector2(1.5f),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = mainContent = new Container
|
||||
Children = new[]
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
solidBackgroundLayer = new Box
|
||||
{
|
||||
avatarPositionTarget = new Container
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider?.Background5 ?? colours.Gray1
|
||||
},
|
||||
background = new UserCoverBackground
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Colour = colours.Gray7,
|
||||
User = User
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = mainContent = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = avatar_size,
|
||||
Child = avatarJumpTarget = new Container
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = avatar = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id)
|
||||
quitText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = Vector2.One
|
||||
Text = "QUIT",
|
||||
Font = OsuFont.Default.With(weight: "Bold", size: 70),
|
||||
Rotation = -22.5f,
|
||||
Colour = OsuColour.Gray(0.3f),
|
||||
Blending = BlendingParameters.Additive
|
||||
},
|
||||
avatarPositionTarget = new Container
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Size = avatar_size,
|
||||
Child = avatarJumpTarget = new Container
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = avatar = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
// Needs to be re-buffered as the avatar is proxied outside of the parent buffered container.
|
||||
Child = avatarQuitTarget = new BufferedContainer
|
||||
{
|
||||
FrameBufferScale = new Vector2(1.5f),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = Vector2.One
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
rankText = new OsuSpriteText
|
||||
{
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Margin = new MarginPadding(4),
|
||||
Text = "-",
|
||||
Font = OsuFont.Style.Title.With(size: 55),
|
||||
},
|
||||
username = new OsuSpriteText
|
||||
{
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Text = User.Username,
|
||||
Font = OsuFont.Style.Heading1,
|
||||
},
|
||||
scoreText = new OsuSpriteText
|
||||
{
|
||||
Alpha = 0,
|
||||
Margin = new MarginPadding(10),
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Font = OsuFont.Style.Heading2,
|
||||
Text = "0 pts"
|
||||
}
|
||||
}
|
||||
},
|
||||
rankText = new OsuSpriteText
|
||||
{
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Margin = new MarginPadding(4),
|
||||
Text = "-",
|
||||
Font = OsuFont.Style.Title.With(size: 55),
|
||||
},
|
||||
username = new OsuSpriteText
|
||||
{
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Text = User.Username,
|
||||
Font = OsuFont.Style.Heading1,
|
||||
},
|
||||
scoreText = new OsuSpriteText
|
||||
{
|
||||
Alpha = 0,
|
||||
Margin = new MarginPadding(10),
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Font = OsuFont.Style.Heading2,
|
||||
Text = "0 pts"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Allow avatar to exist outside of masking for when it jumps around and stuff.
|
||||
AddInternal(avatar.CreateProxy());
|
||||
@@ -239,6 +258,28 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
.FadeIn(200);
|
||||
}
|
||||
|
||||
public PlayerPanelDisplayMode DisplayMode
|
||||
{
|
||||
get => displayMode;
|
||||
set
|
||||
{
|
||||
displayMode = value;
|
||||
if (IsLoaded)
|
||||
updateLayout(false);
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasQuit
|
||||
{
|
||||
get => hasQuit;
|
||||
set
|
||||
{
|
||||
hasQuit = value;
|
||||
if (IsLoaded)
|
||||
updateLayout(false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool horizontal => displayMode == PlayerPanelDisplayMode.Horizontal;
|
||||
|
||||
private Vector2 avatarPosition
|
||||
@@ -275,16 +316,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
scoreText.Hide();
|
||||
username.Hide();
|
||||
|
||||
Background.FadeOut(200, Easing.OutQuint);
|
||||
SolidBackgroundLayer.FadeOut(200, Easing.OutQuint);
|
||||
background.FadeOut(200, Easing.OutQuint);
|
||||
solidBackgroundLayer.FadeOut(200, Easing.OutQuint);
|
||||
|
||||
this.ResizeTo(avatar_size, duration, Easing.OutPow10);
|
||||
break;
|
||||
|
||||
case PlayerPanelDisplayMode.Horizontal:
|
||||
case PlayerPanelDisplayMode.Vertical:
|
||||
Background.FadeIn(200);
|
||||
SolidBackgroundLayer.FadeIn(200);
|
||||
background.FadeIn(200);
|
||||
solidBackgroundLayer.FadeIn(200);
|
||||
|
||||
using (BeginDelayedSequence(100))
|
||||
{
|
||||
@@ -303,14 +344,40 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
|
||||
this.ResizeTo(horizontal ? size_horizontal : size_vertical, duration, Easing.OutPow10);
|
||||
|
||||
rankText.MoveTo(horizontal ? new Vector2(-40, -10) : new Vector2(-70, 0), duration, Easing.OutPow10);
|
||||
rankText.MoveTo(horizontal ? new Vector2(-40, -20) : new Vector2(-70, 0), duration, Easing.OutPow10);
|
||||
username.MoveTo(horizontal ? new Vector2(0, -46) : new Vector2(0, -86), duration, Easing.OutPow10);
|
||||
scoreText.MoveTo(horizontal ? new Vector2(0, -16) : new Vector2(0, -56), duration, Easing.OutPow10);
|
||||
quitText.MoveTo(horizontal ? new Vector2(40, 0) : new Vector2(0, 40), duration, Easing.OutPow10);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
// quit text doesn't fit on avataronly mode.
|
||||
if (HasQuit && displayMode != PlayerPanelDisplayMode.AvatarOnly)
|
||||
quitText.FadeIn(duration, Easing.OutPow10);
|
||||
else
|
||||
quitText.FadeOut(duration, Easing.OutPow10);
|
||||
|
||||
if (HasQuit)
|
||||
{
|
||||
backgroundQuitTarget.GrayscaleTo(1, duration, Easing.OutPow10);
|
||||
avatarQuitTarget.GrayscaleTo(1, duration, Easing.OutPow10);
|
||||
}
|
||||
else
|
||||
{
|
||||
backgroundQuitTarget.GrayscaleTo(0, duration, Easing.OutPow10);
|
||||
avatarQuitTarget.GrayscaleTo(0, duration, Easing.OutPow10);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// Not sure why this is required but it is.
|
||||
avatarQuitTarget.Alpha = Alpha;
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
@@ -347,8 +414,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
if (!matchmakingState.Users.UserDictionary.TryGetValue(User.Id, out MatchmakingUser? userScore))
|
||||
return;
|
||||
|
||||
rankText.Text = userScore.Placement.Ordinalize(CultureInfo.CurrentCulture);
|
||||
rankText.FadeColour(SubScreenResults.ColourForPlacement(userScore.Placement));
|
||||
if (userScore.Placement == null)
|
||||
return;
|
||||
|
||||
rankText.Text = userScore.Placement.Value.Ordinalize(CultureInfo.CurrentCulture);
|
||||
rankText.FadeColour(SubScreenResults.ColourForPlacement(userScore.Placement.Value));
|
||||
scoreText.Text = $"{userScore.Points} pts";
|
||||
});
|
||||
|
||||
@@ -419,7 +489,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
{
|
||||
List<MenuItem> items = new List<MenuItem>
|
||||
{
|
||||
new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, ViewProfile)
|
||||
new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, viewProfile)
|
||||
};
|
||||
|
||||
if (User.Equals(api.LocalUser.Value))
|
||||
@@ -457,7 +527,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
|
||||
bool isUserOnline() => metadataClient?.GetPresence(User.OnlineID) != null;
|
||||
bool canInviteUser() => isUserOnline() && multiplayerClient?.Room?.Users.All(u => u.UserID != User.Id) == true;
|
||||
bool isUserBlocked() => api.Blocks.Any(b => b.TargetID == User.OnlineID);
|
||||
bool isUserBlocked() => api.LocalUserState.Blocks.Any(b => b.TargetID == User.OnlineID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
|
||||
private void onUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() =>
|
||||
{
|
||||
panels.Single(p => p.RoomUser.Equals(user)).Expire();
|
||||
panels.Single(p => p.RoomUser.Equals(user)).HasQuit = true;
|
||||
updateDisplay();
|
||||
});
|
||||
|
||||
@@ -239,8 +239,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
if (client.Room?.MatchState is not MatchmakingRoomState matchmakingState)
|
||||
continue;
|
||||
|
||||
if (matchmakingState.Users.UserDictionary.TryGetValue(panels[i].User.Id, out MatchmakingUser? user))
|
||||
SetLayoutPosition(Children[i], user.Placement);
|
||||
if (matchmakingState.Users.UserDictionary.TryGetValue(panels[i].User.Id, out MatchmakingUser? user) && user.Placement != null)
|
||||
SetLayoutPosition(Children[i], user.Placement.Value);
|
||||
else
|
||||
SetLayoutPosition(Children[i], float.MaxValue);
|
||||
}
|
||||
|
||||
@@ -194,20 +194,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
|
||||
{
|
||||
userStatistics.Clear();
|
||||
|
||||
if (state.Users[client.LocalUser!.UserID].Rounds.Count == 0)
|
||||
var localUserState = state.Users.GetOrAdd(client.LocalUser!.UserID);
|
||||
|
||||
if (localUserState.Rounds.Count == 0)
|
||||
{
|
||||
placementText.Text = "-";
|
||||
placementText.Colour = OsuColour.Gray(1f);
|
||||
return;
|
||||
}
|
||||
|
||||
int overallPlacement = state.Users[client.LocalUser!.UserID].Placement;
|
||||
int? overallPlacement = localUserState.Placement;
|
||||
|
||||
placementText.Text = overallPlacement.Ordinalize(CultureInfo.CurrentCulture);
|
||||
placementText.Colour = ColourForPlacement(overallPlacement);
|
||||
if (overallPlacement != null)
|
||||
{
|
||||
placementText.Text = overallPlacement.Value.Ordinalize(CultureInfo.CurrentCulture);
|
||||
placementText.Colour = ColourForPlacement(overallPlacement.Value);
|
||||
|
||||
int overallPoints = state.Users[client.LocalUser!.UserID].Points;
|
||||
addStatistic(overallPlacement, $"Overall position ({overallPoints} points)");
|
||||
int overallPoints = localUserState.Points;
|
||||
addStatistic(overallPlacement.Value, $"Overall position ({overallPoints} points)");
|
||||
}
|
||||
|
||||
var accuracyOrderedUsers = state.Users.Select(u => (user: u, avgAcc: u.Rounds.Select(r => r.Accuracy).DefaultIfEmpty(0).Average()))
|
||||
.OrderByDescending(t => t.avgAcc)
|
||||
@@ -216,15 +221,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
|
||||
int accuracyPlacement = accuracyOrderedUsers.index + 1;
|
||||
addStatistic(accuracyPlacement, $"Overall accuracy ({accuracyOrderedUsers.info.avgAcc.FormatAccuracy()})");
|
||||
|
||||
var maxComboOrderedUsers = state.Users.Select(u => (user: u, maxCombo: u.Rounds.Max(r => r.MaxCombo)))
|
||||
var maxComboOrderedUsers = state.Users.Select(u => (user: u, maxCombo: u.Rounds.Select(r => r.MaxCombo).DefaultIfEmpty(0).Max()))
|
||||
.OrderByDescending(t => t.maxCombo)
|
||||
.Select((t, i) => (info: t, index: i))
|
||||
.Single(t => t.info.user.UserId == client.LocalUser!.UserID);
|
||||
int maxComboPlacement = maxComboOrderedUsers.index + 1;
|
||||
addStatistic(maxComboPlacement, $"Best max combo ({maxComboOrderedUsers.info.maxCombo}x)");
|
||||
|
||||
var bestPlacement = state.Users[client.LocalUser!.UserID].Rounds.MinBy(r => r.Placement);
|
||||
addStatistic(bestPlacement!.Placement, $"Best round placement (round {bestPlacement.Round})");
|
||||
var bestPlacement = localUserState.Rounds.MinBy(r => r.Placement);
|
||||
if (bestPlacement != null)
|
||||
addStatistic(bestPlacement.Placement, $"Best round placement (round {bestPlacement.Round})");
|
||||
|
||||
void addStatistic(int position, string text) => userStatistics.Add(new PanelUserStatistic(position, text));
|
||||
}
|
||||
@@ -255,27 +261,27 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
|
||||
roomAwards.Clear();
|
||||
|
||||
long maxScore = long.MinValue;
|
||||
int maxScoreUserId = 0;
|
||||
int maxScoreUserId = -1;
|
||||
|
||||
double maxAccuracy = double.MinValue;
|
||||
int maxAccuracyUserId = 0;
|
||||
int maxAccuracyUserId = -1;
|
||||
|
||||
int maxCombo = int.MinValue;
|
||||
int maxComboUserId = 0;
|
||||
int maxComboUserId = -1;
|
||||
|
||||
long maxBonusScore = 0;
|
||||
int maxBonusScoreUserId = 0;
|
||||
int maxBonusScoreUserId = -1;
|
||||
|
||||
long largestScoreDifference = long.MinValue;
|
||||
int largestScoreDifferenceUserId = 0;
|
||||
int largestScoreDifferenceUserId = -1;
|
||||
|
||||
long smallestScoreDifference = long.MaxValue;
|
||||
int smallestScoreDifferenceUserId = 0;
|
||||
int smallestScoreDifferenceUserId = -1;
|
||||
|
||||
for (int round = 1; round <= state.CurrentRound; round++)
|
||||
{
|
||||
long roundHighestScore = long.MinValue;
|
||||
int roundHighestScoreUserId = 0;
|
||||
int roundHighestScoreUserId = -1;
|
||||
|
||||
long roundLowestScore = long.MaxValue;
|
||||
|
||||
@@ -344,11 +350,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
|
||||
}
|
||||
}
|
||||
|
||||
addAward(maxScoreUserId, "Score champ", "Highest score in a single round");
|
||||
if (maxScoreUserId > 0)
|
||||
addAward(maxScoreUserId, "Score champ", "Highest score in a single round");
|
||||
|
||||
addAward(maxAccuracyUserId, "Most accurate", "Highest accuracy in a single round");
|
||||
if (maxAccuracyUserId > 0)
|
||||
addAward(maxAccuracyUserId, "Most accurate", "Highest accuracy in a single round");
|
||||
|
||||
addAward(maxComboUserId, "Top combo", "Highest combo in a single round");
|
||||
if (maxComboUserId > 0)
|
||||
addAward(maxComboUserId, "Top combo", "Highest combo in a single round");
|
||||
|
||||
if (maxBonusScoreUserId > 0)
|
||||
addAward(maxBonusScoreUserId, "Biggest bonus", "Biggest bonus score across all rounds");
|
||||
|
||||
@@ -16,20 +16,23 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Matchmaking.Events;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Gameplay;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
@@ -37,7 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
/// <summary>
|
||||
/// The main matchmaking screen which houses a custom <see cref="ScreenStack"/> through the life cycle of a single session.
|
||||
/// </summary>
|
||||
public partial class ScreenMatchmaking : OsuScreen
|
||||
public partial class ScreenMatchmaking : OsuScreen, IPreviewTrackOwner, IHandlePresentBeatmap
|
||||
{
|
||||
/// <summary>
|
||||
/// Padding between rows of the content.
|
||||
@@ -74,20 +77,37 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
[Resolved]
|
||||
private AudioManager audio { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private PreviewTrackManager previewTrackManager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private MusicController music { get; set; } = null!;
|
||||
|
||||
private readonly MultiplayerRoom room;
|
||||
private readonly MatchmakingChatDisplay chat;
|
||||
|
||||
private Sample? sampleStart;
|
||||
private CancellationTokenSource? downloadCheckCancellation;
|
||||
private int? lastDownloadCheckedBeatmapId;
|
||||
|
||||
private MatchChatDisplay chat = null!;
|
||||
|
||||
public ScreenMatchmaking(MultiplayerRoom room)
|
||||
{
|
||||
this.room = room;
|
||||
|
||||
Activity.Value = new UserActivity.InLobby(room);
|
||||
Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING };
|
||||
|
||||
chat = new MatchmakingChatDisplay(new Room(room))
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Size = new Vector2(700, 130),
|
||||
Margin = new MarginPadding { Bottom = 10, Right = WaveOverlayContainer.WIDTH_PADDING - HORIZONTAL_OVERFLOW_PADDING },
|
||||
Alpha = 0
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -144,13 +164,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Width = 700,
|
||||
Height = 130,
|
||||
Padding = new MarginPadding { Bottom = row_padding },
|
||||
Child = chat = new MatchmakingChatDisplay(new Room(room))
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
Size = new Vector2(700, 130),
|
||||
Margin = new MarginPadding { Bottom = row_padding }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -171,7 +186,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
|
||||
beatmapAvailabilityTracker.Availability.BindValueChanged(onBeatmapAvailabilityChanged, true);
|
||||
|
||||
Footer!.Add(chat.CreateProxy());
|
||||
Footer?.Add(new ChatContainer(chat));
|
||||
}
|
||||
|
||||
private void onRoomUpdated()
|
||||
@@ -224,19 +239,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
// Update global gameplay state to correspond to the new selection.
|
||||
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
|
||||
var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", item.BeatmapID);
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
|
||||
Ruleset.Value = ruleset;
|
||||
Mods.Value = item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
|
||||
if (Beatmap.Value is DummyWorkingBeatmap)
|
||||
if (localBeatmap != null)
|
||||
{
|
||||
if (client.LocalUser!.State == MultiplayerUserState.Ready)
|
||||
client.ChangeState(MultiplayerUserState.Idle).FireAndForget();
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
|
||||
Ruleset.Value = ruleset;
|
||||
Mods.Value = item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
|
||||
// Notify the server that the beatmap has been set and that we are ready to start gameplay.
|
||||
if (client.LocalUser!.State == MultiplayerUserState.Idle)
|
||||
client.ChangeState(MultiplayerUserState.Ready).FireAndForget();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (client.LocalUser!.State == MultiplayerUserState.Idle)
|
||||
client.ChangeState(MultiplayerUserState.Ready).FireAndForget();
|
||||
// Notify the server that we don't have the beatmap.
|
||||
if (client.LocalUser!.State == MultiplayerUserState.Ready)
|
||||
client.ChangeState(MultiplayerUserState.Idle).FireAndForget();
|
||||
}
|
||||
|
||||
client.ChangeBeatmapAvailability(beatmapAvailabilityTracker.Availability.Value).FireAndForget();
|
||||
@@ -275,21 +293,21 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
|
||||
downloadCheckCancellation?.Cancel();
|
||||
|
||||
if (beatmapManager.IsAvailableLocally(new APIBeatmap { OnlineID = item.BeatmapID }))
|
||||
return;
|
||||
|
||||
// In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes.
|
||||
// ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised.
|
||||
beatmapLookupCache
|
||||
.GetBeatmapAsync(item.BeatmapID, (downloadCheckCancellation = new CancellationTokenSource()).Token)
|
||||
.ContinueWith(resolved => Schedule(() =>
|
||||
{
|
||||
var beatmapSet = resolved.GetResultSafely()?.BeatmapSet;
|
||||
APIBeatmapSet? beatmapSet = resolved.GetResultSafely()?.BeatmapSet;
|
||||
|
||||
if (beatmapSet == null)
|
||||
return;
|
||||
|
||||
if (beatmapManager.IsAvailableLocally(new BeatmapSetInfo { OnlineID = beatmapSet.OnlineID }))
|
||||
return;
|
||||
|
||||
beatmapDownloader.Download(beatmapSet);
|
||||
beatmapDownloader.Download(beatmapSet, config.Get<bool>(OsuSetting.PreferNoVideo));
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -308,6 +326,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void OnEntering(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnEntering(e);
|
||||
|
||||
chat.Appear();
|
||||
beginHandlingTrack();
|
||||
}
|
||||
|
||||
public override void OnSuspending(ScreenTransitionEvent e)
|
||||
{
|
||||
chat.Disappear();
|
||||
endHandlingTrack();
|
||||
|
||||
base.OnSuspending(e);
|
||||
}
|
||||
|
||||
private bool exitConfirmed;
|
||||
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
@@ -320,6 +354,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
return true;
|
||||
}
|
||||
|
||||
chat.Disappear().Expire();
|
||||
endHandlingTrack();
|
||||
|
||||
client.LeaveRoom().FireAndForget();
|
||||
return false;
|
||||
}
|
||||
@@ -343,6 +380,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
{
|
||||
base.OnResuming(e);
|
||||
|
||||
chat.Appear();
|
||||
beginHandlingTrack();
|
||||
|
||||
if (e.Last is not MultiplayerPlayerLoader playerLoader)
|
||||
return;
|
||||
|
||||
@@ -352,7 +392,45 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
return;
|
||||
}
|
||||
|
||||
client.ChangeState(MultiplayerUserState.Idle);
|
||||
client.ChangeState(MultiplayerUserState.Idle).FireAndForget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles changes in the track to keep it looping while active.
|
||||
/// </summary>
|
||||
private void beginHandlingTrack()
|
||||
{
|
||||
Beatmap.BindValueChanged(applyLoopingToTrack, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops looping the current track and stops handling further changes to the track.
|
||||
/// </summary>
|
||||
private void endHandlingTrack()
|
||||
{
|
||||
Beatmap.ValueChanged -= applyLoopingToTrack;
|
||||
Beatmap.Value.Track.Looping = false;
|
||||
|
||||
previewTrackManager.StopAnyPlaying(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked on changes to the beatmap to loop the track. See: <see cref="beginHandlingTrack"/>.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap change event.</param>
|
||||
private void applyLoopingToTrack(ValueChangedEvent<WorkingBeatmap> beatmap)
|
||||
{
|
||||
if (!this.IsCurrentScreen())
|
||||
return;
|
||||
|
||||
beatmap.NewValue.PrepareTrackForPreview(true);
|
||||
music.EnsurePlayingSomething();
|
||||
}
|
||||
|
||||
public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
|
||||
{
|
||||
// Do nothing to prevent the user from potentially being kicked out
|
||||
// of gameplay due to the screen performer's internal processes.
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
@@ -367,5 +445,32 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
client.LoadRequested -= onLoadRequested;
|
||||
}
|
||||
}
|
||||
|
||||
// Contains the chat display and a context menu container for it. Shared lifetime with the chat display (expires along with it).
|
||||
private partial class ChatContainer : CompositeDrawable
|
||||
{
|
||||
public override double LifetimeStart => chat.LifetimeStart;
|
||||
public override double LifetimeEnd => chat.LifetimeEnd;
|
||||
|
||||
private readonly MatchmakingChatDisplay chat;
|
||||
|
||||
public ChatContainer(MatchmakingChatDisplay chat)
|
||||
{
|
||||
this.chat = chat;
|
||||
|
||||
Anchor = Anchor.BottomRight;
|
||||
Origin = Anchor.BottomRight;
|
||||
|
||||
// This component is added to the screen footer which is only about 50px high.
|
||||
// Therefore, it's given a large absolute size to give the context menu enough space to display correctly.
|
||||
Size = new Vector2(700);
|
||||
|
||||
InternalChild = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = chat
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user