1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-05 10:23:20 +08:00

Merge branch 'ppy:master' into strain-reduction-removal

This commit is contained in:
KermitNuggies 2024-11-30 12:05:44 +13:00 committed by GitHub
commit a60088c5ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
92 changed files with 1730 additions and 725 deletions

View File

@ -114,7 +114,10 @@ jobs:
dotnet-version: "8.0.x" dotnet-version: "8.0.x"
- name: Install .NET workloads - name: Install .NET workloads
run: dotnet workload install android # since windows image 20241113.3.0, not specifying a version here
# installs the .NET 7 version of android workload for very unknown reasons.
# revisit once we upgrade to .NET 9, it's probably fixed there.
run: dotnet workload install android --version (dotnet --version)
- name: Compile - name: Compile
run: dotnet build -c Debug osu.Android.slnf run: dotnet build -c Debug osu.Android.slnf

View File

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

View File

@ -15,6 +15,7 @@ using osu.Framework.Threading;
using osu.Game; using osu.Game;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Online;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
@ -47,6 +48,9 @@ namespace osu.Desktop
[Resolved] [Resolved]
private MultiplayerClient multiplayerClient { get; set; } = null!; private MultiplayerClient multiplayerClient { get; set; } = null!;
[Resolved]
private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!;
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } = null!; private OsuConfigManager config { get; set; } = null!;
@ -117,7 +121,9 @@ namespace osu.Desktop
status.BindValueChanged(_ => schedulePresenceUpdate()); status.BindValueChanged(_ => schedulePresenceUpdate());
activity.BindValueChanged(_ => schedulePresenceUpdate()); activity.BindValueChanged(_ => schedulePresenceUpdate());
privacyMode.BindValueChanged(_ => schedulePresenceUpdate()); privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
multiplayerClient.RoomUpdated += onRoomUpdated; multiplayerClient.RoomUpdated += onRoomUpdated;
statisticsProvider.StatisticsUpdated += onStatisticsUpdated;
} }
private void onReady(object _, ReadyMessage __) private void onReady(object _, ReadyMessage __)
@ -133,6 +139,8 @@ namespace osu.Desktop
private void onRoomUpdated() => schedulePresenceUpdate(); private void onRoomUpdated() => schedulePresenceUpdate();
private void onStatisticsUpdated(UserStatisticsUpdate _) => schedulePresenceUpdate();
private ScheduledDelegate? presenceUpdateDelegate; private ScheduledDelegate? presenceUpdateDelegate;
private void schedulePresenceUpdate() private void schedulePresenceUpdate()
@ -167,7 +175,7 @@ namespace osu.Desktop
presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation)); presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0) if (activity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0)
{ {
presence.Buttons = new[] presence.Buttons = new[]
{ {
@ -229,10 +237,8 @@ namespace osu.Desktop
presence.Assets.LargeImageText = string.Empty; presence.Assets.LargeImageText = string.Empty;
else else
{ {
if (user.Value.RulesetsStatistics != null && user.Value.RulesetsStatistics.TryGetValue(ruleset.Value.ShortName, out UserStatistics? statistics)) var statistics = statisticsProvider.GetStatisticsFor(ruleset.Value);
presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty); presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics?.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty);
else
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
} }
// small image // small image
@ -327,25 +333,14 @@ namespace osu.Desktop
return true; return true;
} }
private static int? getBeatmapID(UserActivity activity)
{
switch (activity)
{
case UserActivity.InGame game:
return game.BeatmapID;
case UserActivity.EditingBeatmap edit:
return edit.BeatmapID;
}
return null;
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
if (multiplayerClient.IsNotNull()) if (multiplayerClient.IsNotNull())
multiplayerClient.RoomUpdated -= onRoomUpdated; multiplayerClient.RoomUpdated -= onRoomUpdated;
if (statisticsProvider.IsNotNull())
statisticsProvider.StatisticsUpdated -= onStatisticsUpdated;
client.Dispose(); client.Dispose();
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }

View File

@ -4,28 +4,54 @@
using System.Collections.Generic; using System.Collections.Generic;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Objects; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Benchmarks namespace osu.Game.Benchmarks
{ {
public class BenchmarkUnstableRate : BenchmarkTest public class BenchmarkUnstableRate : BenchmarkTest
{ {
private List<HitEvent> events = null!; private readonly List<List<HitEvent>> incrementalEventLists = new List<List<HitEvent>>();
public override void SetUp() public override void SetUp()
{ {
base.SetUp(); base.SetUp();
events = new List<HitEvent>();
for (int i = 0; i < 1000; i++) var events = new List<HitEvent>();
events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, new HitObject(), null, null));
for (int i = 0; i < 2048; i++)
{
// Ensure the object has hit windows populated.
var hitObject = new HitCircle();
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, hitObject, null, null));
incrementalEventLists.Add(new List<HitEvent>(events));
}
} }
[Benchmark] [Benchmark]
public void CalculateUnstableRate() public void CalculateUnstableRate()
{ {
_ = events.CalculateUnstableRate(); for (int i = 0; i < 2048; i++)
{
var events = incrementalEventLists[i];
_ = events.CalculateUnstableRate();
}
}
[Benchmark]
public void CalculateUnstableRateUsingIncrementalCalculation()
{
HitEventExtensions.UnstableRateCalculationResult? last = null;
for (int i = 0; i < 2048; i++)
{
var events = incrementalEventLists[i];
last = events.CalculateUnstableRate(last);
}
} }
} }
} }

View File

@ -164,10 +164,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private Drawable getResult(HitResult result) private Drawable getResult(HitResult result)
{ {
if (!hit_result_mapping.ContainsKey(result)) if (!hit_result_mapping.TryGetValue(result, out var value))
return null; return null;
string filename = this.GetManiaSkinConfig<string>(hit_result_mapping[result])?.Value string filename = this.GetManiaSkinConfig<string>(value)?.Value
?? default_hit_result_skin_filenames[result]; ?? default_hit_result_skin_filenames[result];
var animation = this.GetAnimation(filename, true, true, frameLength: 1000 / 20d); var animation = this.GetAnimation(filename, true, true, frameLength: 1000 / 20d);

View File

@ -63,18 +63,18 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Origin = Anchor.Centre, Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-top"), Texture = source.GetTexture("spinner-top"),
}, },
fixedMiddle = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-middle"),
},
spinningMiddle = new Sprite spinningMiddle = new Sprite
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-middle2"), Texture = source.GetTexture("spinner-middle2"),
}, },
fixedMiddle = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-middle"),
},
} }
}); });

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
@ -64,6 +65,10 @@ namespace osu.Game.Tests
// Beatmap must be imported before the collection manager is loaded. // Beatmap must be imported before the collection manager is loaded.
if (withBeatmap) if (withBeatmap)
BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely(); BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely();
// the logic for setting the initial ruleset exists in OsuGame rather than OsuGameBase.
// the ruleset bindable is not meant to be nullable, so assign any ruleset in here.
Ruleset.Value = RulesetStore.AvailableRulesets.First();
} }
} }
} }

View File

@ -20,12 +20,53 @@ namespace osu.Game.Tests.NonVisual.Ranking
public void TestDistributedHits() public void TestDistributedHits()
{ {
var events = Enumerable.Range(-5, 11) var events = Enumerable.Range(-5, 11)
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)); .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null))
.ToList();
var unstableRate = new UnstableRate(events); var unstableRate = new UnstableRate(events);
Assert.IsNotNull(unstableRate.Value); Assert.IsNotNull(unstableRate.Value);
Assert.IsTrue(Precision.AlmostEquals(unstableRate.Value.Value, 10 * Math.Sqrt(10))); Assert.AreEqual(unstableRate.Value.Value, 10 * Math.Sqrt(10), Precision.DOUBLE_EPSILON);
}
[Test]
public void TestDistributedHitsIncrementalRewind()
{
var events = Enumerable.Range(-5, 11)
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null))
.ToList();
HitEventExtensions.UnstableRateCalculationResult result = null;
for (int i = 0; i < events.Count; i++)
{
result = events.GetRange(0, i + 1)
.CalculateUnstableRate(result);
}
result = events.GetRange(0, 2).CalculateUnstableRate(result);
Assert.IsNotNull(result!.Result);
Assert.AreEqual(5, result.Result, Precision.DOUBLE_EPSILON);
}
[Test]
public void TestDistributedHitsIncremental()
{
var events = Enumerable.Range(-5, 11)
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null))
.ToList();
HitEventExtensions.UnstableRateCalculationResult result = null;
for (int i = 0; i < events.Count; i++)
{
result = events.GetRange(0, i + 1)
.CalculateUnstableRate(result);
}
Assert.IsNotNull(result!.Result);
Assert.AreEqual(10 * Math.Sqrt(10), result.Result, Precision.DOUBLE_EPSILON);
} }
[Test] [Test]

View File

@ -527,8 +527,11 @@ namespace osu.Game.Tests.Visual.Editing
checkPlacementSampleBank(HitSampleInfo.BANK_NORMAL); checkPlacementSampleBank(HitSampleInfo.BANK_NORMAL);
checkPlacementSampleAdditionBank(HitSampleInfo.BANK_NORMAL); checkPlacementSampleAdditionBank(HitSampleInfo.BANK_NORMAL);
void checkPlacementSampleBank(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected)); void checkPlacementSampleBank(string expected) => AddAssert($"Placement sample is {expected}",
void checkPlacementSampleAdditionBank(string expected) => AddAssert($"Placement sample addition is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name != HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected)); () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected));
void checkPlacementSampleAdditionBank(string expected) => AddAssert($"Placement sample addition is {expected}",
() => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name != HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected));
} }
[Test] [Test]
@ -781,15 +784,39 @@ namespace osu.Game.Tests.Visual.Editing
setAdditionBankViaPopover(HitSampleInfo.BANK_SOFT); setAdditionBankViaPopover(HitSampleInfo.BANK_SOFT);
dismissPopover(); dismissPopover();
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH); assertNoChanges();
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0])); AddStep("select first object", () =>
{
EditorBeatmap.SelectedHitObjects.Clear();
EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0]);
});
assertNoChanges();
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH); AddStep("select second object", () =>
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL); {
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT); EditorBeatmap.SelectedHitObjects.Clear();
EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[1]);
});
assertNoChanges();
AddStep("select first object", () =>
{
EditorBeatmap.SelectedHitObjects.Clear();
EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0]);
});
assertNoChanges();
void assertNoChanges()
{
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL);
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_SOFT);
}
} }
private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () => private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () =>
@ -883,11 +910,12 @@ namespace osu.Game.Tests.Visual.Editing
return h.Samples.All(o => o.Volume == volume); return h.Samples.All(o => o.Volume == volume);
}); });
private void hitObjectNodeHasSampleVolume(int objectIndex, int nodeIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has volume {volume}", () => private void hitObjectNodeHasSampleVolume(int objectIndex, int nodeIndex, int volume) => AddAssert(
{ $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has volume {volume}", () =>
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; {
return h is not null && h.NodeSamples[nodeIndex].All(o => o.Volume == volume); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
}); return h is not null && h.NodeSamples[nodeIndex].All(o => o.Volume == volume);
});
private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () => private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () =>
{ {
@ -944,29 +972,33 @@ namespace osu.Game.Tests.Visual.Editing
return h.Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); return h.Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
}); });
private void hitObjectNodeHasSamples(int objectIndex, int nodeIndex, params string[] samples) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has samples {string.Join(',', samples)}", () => private void hitObjectNodeHasSamples(int objectIndex, int nodeIndex, params string[] samples) => AddAssert(
{ $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has samples {string.Join(',', samples)}", () =>
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; {
return h is not null && h.NodeSamples[nodeIndex].Select(s => s.Name).SequenceEqual(samples); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
}); return h is not null && h.NodeSamples[nodeIndex].Select(s => s.Name).SequenceEqual(samples);
});
private void hitObjectNodeHasSampleBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has bank {bank}", () => private void hitObjectNodeHasSampleBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has bank {bank}",
{ () =>
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; {
return h is not null && h.NodeSamples[nodeIndex].All(o => o.Bank == bank); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
}); return h is not null && h.NodeSamples[nodeIndex].All(o => o.Bank == bank);
});
private void hitObjectNodeHasSampleNormalBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has normal bank {bank}", () => private void hitObjectNodeHasSampleNormalBank(int objectIndex, int nodeIndex, string bank) => AddAssert(
{ $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has normal bank {bank}", () =>
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; {
return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
}); return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
});
private void hitObjectNodeHasSampleAdditionBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has addition bank {bank}", () => private void hitObjectNodeHasSampleAdditionBank(int objectIndex, int nodeIndex, string bank) => AddAssert(
{ $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has addition bank {bank}", () =>
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; {
return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
}); return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
});
private void editorTimeIs(double time) => AddAssert($"editor time is {time}", () => Precision.AlmostEquals(EditorClock.CurrentTimeAccurate, time, 1)); private void editorTimeIs(double time) => AddAssert($"editor time is {time}", () => Precision.AlmostEquals(EditorClock.CurrentTimeAccurate, time, 1));
} }

View File

@ -10,11 +10,13 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Login; using osu.Game.Overlays.Login;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Tests.Visual.Online;
using osu.Game.Users; using osu.Game.Users;
using osu.Game.Users.Drawables; using osu.Game.Users.Drawables;
using osuTK.Input; using osuTK.Input;
@ -31,6 +33,9 @@ namespace osu.Game.Tests.Visual.Menus
[Resolved] [Resolved]
private OsuConfigManager configManager { get; set; } = null!; private OsuConfigManager configManager { get; set; } = null!;
[Cached(typeof(LocalUserStatisticsProvider))]
private readonly TestSceneUserPanel.TestUserStatisticsProvider statisticsProvider = new TestSceneUserPanel.TestUserStatisticsProvider();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -170,6 +175,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "88800088"); AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "88800088");
assertAPIState(APIState.Online); assertAPIState(APIState.Online);
AddStep("feed statistics", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value));
AddStep("click on flag", () => AddStep("click on flag", () =>
{ {
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<UpdateableFlag>().First()); InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<UpdateableFlag>().First());

View File

@ -3,8 +3,10 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -73,5 +75,57 @@ namespace osu.Game.Tests.Visual.Menus
((StarFountain)Children[1]).Shoot(-1); ((StarFountain)Children[1]).Shoot(-1);
}); });
} }
[Test]
public void TestGameplayStarFountainsSetting()
{
Bindable<bool> starFountainsEnabled = null!;
AddStep("load configuration", () =>
{
var config = new OsuConfigManager(LocalStorage);
starFountainsEnabled = config.GetBindable<bool>(OsuSetting.StarFountains);
});
AddStep("make fountains", () =>
{
Children = new Drawable[]
{
new KiaiGameplayFountains.GameplayStarFountain
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
X = 75,
},
new KiaiGameplayFountains.GameplayStarFountain
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
X = -75,
},
};
});
AddStep("enable KiaiStarEffects", () => starFountainsEnabled.Value = true);
AddRepeatStep("activate fountains (enabled)", () =>
{
((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1);
((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1);
}, 100);
AddStep("disable KiaiStarEffects", () => starFountainsEnabled.Value = false);
AddRepeatStep("attempt to activate fountains (disabled)", () =>
{
((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1);
((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1);
}, 100);
AddStep("re-enable KiaiStarEffects", () => starFountainsEnabled.Value = true);
AddRepeatStep("activate fountains (re-enabled)", () =>
{
((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1);
((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1);
}, 100);
}
} }
} }

View File

@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Gain", () => AddStep("Gain", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Loss", () => AddStep("Loss", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Tiny increase in PP", () => AddStep("Tiny increase in PP", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("No change 1", () => AddStep("No change 1", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -170,7 +170,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Was null", () => AddStep("Was null", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Became null", () => AddStep("Became null", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {

View File

@ -354,6 +354,23 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("retry count is 1", () => player.RestartCount == 1); AddAssert("retry count is 1", () => player.RestartCount == 1);
} }
[Test]
public void TestLastScoreNullAfterExitingPlayer()
{
AddUntilStep("wait for last play null", getLastPlay, () => Is.Null);
var getOriginalPlayer = playToCompletion();
AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType<HotkeyRetryOverlay>().First().Action());
AddUntilStep("wait for last play matches player", getLastPlay, () => Is.EqualTo(getOriginalPlayer().Score.ScoreInfo));
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player);
AddStep("exit player", () => (Game.ScreenStack.CurrentScreen as Player)?.Exit());
AddUntilStep("wait for last play null", getLastPlay, () => Is.Null);
ScoreInfo getLastPlay() => Game.Dependencies.Get<SessionStatics>().Get<ScoreInfo>(Static.LastLocalUserScore);
}
[Test] [Test]
public void TestRetryImmediatelyAfterCompletion() public void TestRetryImmediatelyAfterCompletion()
{ {

View File

@ -457,6 +457,61 @@ namespace osu.Game.Tests.Visual.Online
waitForChannel1Visible(); waitForChannel1Visible();
} }
[Test]
public void TestPublicChannelsSortedByName()
{
// Intentionally join back to front.
AddStep("Show overlay with channel 2", () =>
{
channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel2);
chatOverlay.Show();
});
AddUntilStep("second channel is at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel2);
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddUntilStep("first channel is at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel1);
AddStep("message in channel 2", () =>
{
testChannel2.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } });
});
AddUntilStep("first channel still at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel1);
ChannelListItem getFirstVisiblePublicChannel() =>
chatOverlay.ChildrenOfType<ChannelList>().Single().PublicChannelGroup.ItemFlow.FlowingChildren.OfType<ChannelListItem>().First(item => item.Channel.Type == ChannelType.Public);
}
[Test]
public void TestPrivateChannelsSortedByRecent()
{
Channel pmChannel1 = createPrivateChannel();
Channel pmChannel2 = createPrivateChannel();
joinChannel(pmChannel1);
joinChannel(pmChannel2);
AddStep("Show overlay", () => chatOverlay.Show());
AddUntilStep("first channel is at top of list", () => getFirstVisiblePMChannel().Channel == pmChannel1);
AddStep("message in channel 2", () =>
{
pmChannel2.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } });
});
AddUntilStep("wait for first channel raised to top of list", () => getFirstVisiblePMChannel().Channel == pmChannel2);
AddStep("message in channel 1", () =>
{
pmChannel1.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } });
});
AddUntilStep("wait for first channel raised to top of list", () => getFirstVisiblePMChannel().Channel == pmChannel1);
ChannelListItem getFirstVisiblePMChannel() =>
chatOverlay.ChildrenOfType<ChannelList>().Single().PrivateChannelGroup.ItemFlow.FlowingChildren.OfType<ChannelListItem>().First(item => item.Channel.Type == ChannelType.PM);
}
[Test] [Test]
public void TestKeyboardNewChannel() public void TestKeyboardNewChannel()
{ {

View File

@ -0,0 +1,179 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
{
public partial class TestSceneLocalUserStatisticsProvider : OsuTestScene
{
private LocalUserStatisticsProvider statisticsProvider = null!;
private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("clear statistics", () => serverSideStatistics.Clear());
setUser(1000);
AddStep("setup provider", () =>
{
OsuTextFlowContainer text;
((DummyAPIAccess)API).HandleRequest = r =>
{
switch (r)
{
case GetUserRequest userRequest:
int userId = int.Parse(userRequest.Lookup);
string rulesetName = userRequest.Ruleset!.ShortName;
var response = new APIUser
{
Id = userId,
Statistics = tryGetStatistics(userId, rulesetName)
};
userRequest.TriggerSuccess(response);
return true;
default:
return false;
}
};
Clear();
Add(statisticsProvider = new LocalUserStatisticsProvider());
Add(text = new OsuTextFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
statisticsProvider.StatisticsUpdated += update =>
{
text.Clear();
foreach (var ruleset in Dependencies.Get<RulesetStore>().AvailableRulesets)
{
text.AddText(statisticsProvider.GetStatisticsFor(ruleset) is UserStatistics statistics
? $"{ruleset.Name} statistics: (total score: {statistics.TotalScore})"
: $"{ruleset.Name} statistics: (null)");
text.NewLine();
}
text.AddText($"latest update: {update.Ruleset}"
+ $" ({(update.OldStatistics?.TotalScore.ToString() ?? "null")} -> {update.NewStatistics.TotalScore})");
};
Ruleset.Value = new OsuRuleset().RulesetInfo;
});
}
[Test]
public void TestInitialStatistics()
{
AddAssert("osu statistics populated", () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(4_000_000));
AddAssert("taiko statistics populated", () => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(3_000_000));
AddAssert("catch statistics populated", () => statisticsProvider.GetStatisticsFor(new CatchRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(2_000_000));
AddAssert("mania statistics populated", () => statisticsProvider.GetStatisticsFor(new ManiaRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(1_000_000));
}
[Test]
public void TestUserChanges()
{
setUser(1001);
AddStep("update statistics for user 1000", () =>
{
serverSideStatistics[(1000, "osu")] = new UserStatistics { TotalScore = 5_000_000 };
serverSideStatistics[(1000, "taiko")] = new UserStatistics { TotalScore = 6_000_000 };
});
AddAssert("statistics matches user 1001 in osu",
() => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(4_000_000));
AddAssert("statistics matches user 1001 in taiko",
() => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(3_000_000));
setUser(1000, false);
AddAssert("statistics matches user 1000 in osu",
() => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(5_000_000));
AddAssert("statistics matches user 1000 in taiko",
() => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(6_000_000));
}
[Test]
public void TestRefetchStatistics()
{
UserStatisticsUpdate? update = null;
setUser(1001);
AddStep("update statistics server side",
() => serverSideStatistics[(1001, "osu")] = new UserStatistics { TotalScore = 9_000_000 });
AddAssert("statistics match old score",
() => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(4_000_000));
AddStep("setup event", () =>
{
update = null;
statisticsProvider.StatisticsUpdated -= onStatisticsUpdated;
statisticsProvider.StatisticsUpdated += onStatisticsUpdated;
});
AddStep("request refetch", () => statisticsProvider.RefetchStatistics(new OsuRuleset().RulesetInfo));
AddUntilStep("statistics update raised",
() => update?.NewStatistics.TotalScore,
() => Is.EqualTo(9_000_000));
AddAssert("statistics match new score",
() => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(9_000_000));
void onStatisticsUpdated(UserStatisticsUpdate u) => update = u;
}
private UserStatistics tryGetStatistics(int userId, string rulesetName)
=> serverSideStatistics.TryGetValue((userId, rulesetName), out var stats) ? stats : new UserStatistics();
private void setUser(int userId, bool generateStatistics = true)
{
AddStep($"set local user to {userId}", () =>
{
if (generateStatistics)
{
serverSideStatistics[(userId, "osu")] = new UserStatistics { TotalScore = 4_000_000 };
serverSideStatistics[(userId, "taiko")] = new UserStatistics { TotalScore = 3_000_000 };
serverSideStatistics[(userId, "fruits")] = new UserStatistics { TotalScore = 2_000_000 };
serverSideStatistics[(userId, "mania")] = new UserStatistics { TotalScore = 1_000_000 };
}
((DummyAPIAccess)API).LocalUser.Value = new APIUser { Id = userId };
});
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -11,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -24,17 +23,20 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture] [TestFixture]
public partial class TestSceneUserPanel : OsuTestScene public partial class TestSceneUserPanel : OsuTestScene
{ {
private readonly Bindable<UserActivity> activity = new Bindable<UserActivity>(); private readonly Bindable<UserActivity?> activity = new Bindable<UserActivity?>();
private readonly Bindable<UserStatus?> status = new Bindable<UserStatus?>(); private readonly Bindable<UserStatus?> status = new Bindable<UserStatus?>();
private UserGridPanel boundPanel1; private UserGridPanel boundPanel1 = null!;
private TestUserListPanel boundPanel2; private TestUserListPanel boundPanel2 = null!;
[Cached] [Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
[Cached(typeof(LocalUserStatisticsProvider))]
private readonly TestUserStatisticsProvider statisticsProvider = new TestUserStatisticsProvider();
[Resolved] [Resolved]
private IRulesetStore rulesetStore { get; set; } private IRulesetStore rulesetStore { get; set; } = null!;
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
@ -42,7 +44,11 @@ namespace osu.Game.Tests.Visual.Online
activity.Value = null; activity.Value = null;
status.Value = null; status.Value = null;
Child = new FillFlowContainer Remove(statisticsProvider, false);
Clear();
Add(statisticsProvider);
Add(new FillFlowContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -108,7 +114,7 @@ namespace osu.Game.Tests.Visual.Online
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
}) { Width = 300 } }) { Width = 300 }
} }
}; });
boundPanel1.Status.BindTo(status); boundPanel1.Status.BindTo(status);
boundPanel1.Activity.BindTo(activity); boundPanel1.Activity.BindTo(activity);
@ -162,24 +168,21 @@ namespace osu.Game.Tests.Visual.Online
{ {
AddStep("update statistics", () => AddStep("update statistics", () =>
{ {
API.UpdateStatistics(new UserStatistics statisticsProvider.UpdateStatistics(new UserStatistics
{ {
GlobalRank = RNG.Next(100000), GlobalRank = RNG.Next(100000),
CountryRank = RNG.Next(100000) CountryRank = RNG.Next(100000)
}); }, Ruleset.Value);
}); });
AddStep("set statistics to something big", () => AddStep("set statistics to something big", () =>
{ {
API.UpdateStatistics(new UserStatistics statisticsProvider.UpdateStatistics(new UserStatistics
{ {
GlobalRank = RNG.Next(1_000_000, 100_000_000), GlobalRank = RNG.Next(1_000_000, 100_000_000),
CountryRank = RNG.Next(1_000_000, 100_000_000) CountryRank = RNG.Next(1_000_000, 100_000_000)
}); }, Ruleset.Value);
});
AddStep("set statistics to empty", () =>
{
API.UpdateStatistics(new UserStatistics());
}); });
AddStep("set statistics to empty", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value));
} }
private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!);
@ -201,5 +204,11 @@ namespace osu.Game.Tests.Visual.Online
public new TextFlowContainer LastVisitMessage => base.LastVisitMessage; public new TextFlowContainer LastVisitMessage => base.LastVisitMessage;
} }
public partial class TestUserStatisticsProvider : LocalUserStatisticsProvider
{
public new void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset, Action<UserStatisticsUpdate>? callback = null)
=> base.UpdateStatistics(newStatistics, ruleset, callback);
}
} }
} }

View File

@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Online
AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v)); AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v));
AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v)); AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v));
AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v)); AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v));
AddSliderStep("playcount", 0, 999, 0, v => update(s => s.PlayCount = v)); AddSliderStep("playcount", 0, 1500, 1, v => update(s => s.PlayCount = v));
AddStep("create", () => AddStep("create", () =>
{ {
Clear(); Clear();
@ -66,8 +66,8 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestPlayCountRankingTier() public void TestPlayCountRankingTier()
{ {
AddAssert("1 before silver", () => DailyChallengeStatsTooltip.TierForPlayCount(30) == RankingTier.Bronze); AddAssert("1 before silver", () => DailyChallengeStatsTooltip.TierForPlayCount(29) == RankingTier.Bronze);
AddAssert("first silver", () => DailyChallengeStatsTooltip.TierForPlayCount(31) == RankingTier.Silver); AddAssert("first silver", () => DailyChallengeStatsTooltip.TierForPlayCount(30) == RankingTier.Silver);
} }
} }
} }

View File

@ -25,6 +25,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
protected override bool UseOnlineAPI => false; protected override bool UseOnlineAPI => false;
private LocalUserStatisticsProvider statisticsProvider = null!;
private UserStatisticsWatcher watcher = null!; private UserStatisticsWatcher watcher = null!;
[Resolved] [Resolved]
@ -107,7 +108,9 @@ namespace osu.Game.Tests.Visual.Online
AddStep("create watcher", () => AddStep("create watcher", () =>
{ {
Child = watcher = new UserStatisticsWatcher(); Clear();
Add(statisticsProvider = new LocalUserStatisticsProvider());
Add(watcher = new UserStatisticsWatcher(statisticsProvider));
}); });
} }
@ -123,7 +126,7 @@ namespace osu.Game.Tests.Visual.Online
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
@ -146,7 +149,7 @@ namespace osu.Game.Tests.Visual.Online
// note ordering - in this test processing completes *before* the registration is added. // note ordering - in this test processing completes *before* the registration is added.
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
@ -164,7 +167,7 @@ namespace osu.Game.Tests.Visual.Online
long scoreId = getScoreId(); long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
@ -191,7 +194,7 @@ namespace osu.Game.Tests.Visual.Online
long scoreId = getScoreId(); long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
@ -212,7 +215,7 @@ namespace osu.Game.Tests.Visual.Online
long scoreId = getScoreId(); long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
@ -241,7 +244,7 @@ namespace osu.Game.Tests.Visual.Online
feignScoreProcessing(userId, ruleset, 6_000_000); feignScoreProcessing(userId, ruleset, 6_000_000);
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(secondScoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(secondScoreId, ruleset, receivedUpdate => update = receivedUpdate);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, secondScoreId)); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, secondScoreId));
@ -259,15 +262,14 @@ namespace osu.Game.Tests.Visual.Online
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
AddUntilStep("update received", () => update != null); AddUntilStep("update received", () => update != null);
AddAssert("local user values are correct", () => dummyAPI.LocalUser.Value.Statistics.TotalScore, () => Is.EqualTo(5_000_000)); AddAssert("statistics values are correct", () => statisticsProvider.GetStatisticsFor(ruleset)!.TotalScore, () => Is.EqualTo(5_000_000));
AddAssert("statistics values are correct", () => dummyAPI.Statistics.Value!.TotalScore, () => Is.EqualTo(5_000_000));
} }
private int nextUserId = 2000; private int nextUserId = 2000;
@ -289,7 +291,7 @@ namespace osu.Game.Tests.Visual.Online
}); });
} }
private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<UserStatisticsUpdate> onUpdateReady) => private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<ScoreBasedUserStatisticsUpdate> onUpdateReady) =>
AddStep("register for updates", () => AddStep("register for updates", () =>
{ {
watcher.RegisterForStatisticsUpdateAfter( watcher.RegisterForStatisticsUpdateAfter(

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Net; using System.Net;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -31,7 +32,7 @@ namespace osu.Game.Tests.Visual.Playlists
private const int scores_per_result = 10; private const int scores_per_result = 10;
private const int real_user_position = 200; private const int real_user_position = 200;
private TestResultsScreen resultsScreen = null!; private ResultsScreen resultsScreen = null!;
private int lowestScoreId; // Score ID of the lowest score in the list. private int lowestScoreId; // Score ID of the lowest score in the list.
private int highestScoreId; // Score ID of the highest score in the list. private int highestScoreId; // Score ID of the highest score in the list.
@ -68,11 +69,11 @@ namespace osu.Game.Tests.Visual.Playlists
} }
[Test] [Test]
public void TestShowWithUserScore() public void TestShowUserScore()
{ {
AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResults(() => userScore); createResultsWithScore(() => userScore);
waitForDisplay(); waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded); AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded);
@ -81,11 +82,24 @@ namespace osu.Game.Tests.Visual.Playlists
} }
[Test] [Test]
public void TestShowNullUserScore() public void TestShowUserBest()
{
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createUserBestResults();
waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.UserID == userScore.UserID).State == PanelState.Expanded);
AddAssert($"score panel position is {real_user_position}",
() => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.UserID == userScore.UserID).ScorePosition.Value == real_user_position);
}
[Test]
public void TestShowNonUserScores()
{ {
AddStep("bind user score info handler", () => bindHandler()); AddStep("bind user score info handler", () => bindHandler());
createResults(); createUserBestResults();
waitForDisplay(); waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
@ -96,7 +110,7 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
AddStep("bind user score info handler", () => bindHandler(true, userScore)); AddStep("bind user score info handler", () => bindHandler(true, userScore));
createResults(() => userScore); createResultsWithScore(() => userScore);
waitForDisplay(); waitForDisplay();
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1); AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1);
@ -104,11 +118,11 @@ namespace osu.Game.Tests.Visual.Playlists
} }
[Test] [Test]
public void TestShowNullUserScoreWithDelay() public void TestShowNonUserScoresWithDelay()
{ {
AddStep("bind delayed handler", () => bindHandler(true)); AddStep("bind delayed handler", () => bindHandler(true));
createResults(); createUserBestResults();
waitForDisplay(); waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
@ -119,7 +133,7 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
AddStep("bind delayed handler", () => bindHandler(true)); AddStep("bind delayed handler", () => bindHandler(true));
createResults(); createUserBestResults();
waitForDisplay(); waitForDisplay();
for (int i = 0; i < 2; i++) for (int i = 0; i < 2; i++)
@ -127,13 +141,16 @@ namespace osu.Game.Tests.Visual.Playlists
int beforePanelCount = 0; int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false)); AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddAssert("right loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
} }
} }
@ -142,29 +159,36 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
AddStep("bind delayed handler with scores", () => bindHandler(delayed: true)); AddStep("bind delayed handler with scores", () => bindHandler(delayed: true));
createResults(); createUserBestResults();
waitForDisplay(); waitForDisplay();
int beforePanelCount = 0; int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false)); AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddAssert("right loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true)); AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true));
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false)); AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert("count not increased", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount); AddAssert("count not increased", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddAssert("right loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
AddAssert("no placeholders shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.Zero); AddAssert("no placeholders shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.Zero);
} }
@ -173,7 +197,7 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResults(() => userScore); createResultsWithScore(() => userScore);
waitForDisplay(); waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(true)); AddStep("bind delayed handler", () => bindHandler(true));
@ -183,30 +207,36 @@ namespace osu.Game.Tests.Visual.Playlists
int beforePanelCount = 0; int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to left", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToStart(false)); AddStep("scroll to left", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToStart(false));
AddAssert("left loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible);
AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden); AddAssert("left loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden);
} }
} }
/// <summary>
/// Shows the <see cref="TestUserBestResultsScreen"/> with no scores provided by the API.
/// </summary>
[Test] [Test]
public void TestShowWithNoScores() public void TestShowUserBestWithNoScoresPresent()
{ {
AddStep("bind user score info handler", () => bindHandler(noScores: true)); AddStep("bind user score info handler", () => bindHandler(noScores: true));
createResults(); createUserBestResults();
AddAssert("no scores visible", () => !resultsScreen.ScorePanelList.GetScorePanels().Any()); AddAssert("no scores visible", () => !resultsScreen.ChildrenOfType<ScorePanelList>().Single().GetScorePanels().Any());
AddAssert("placeholder shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.EqualTo(1)); AddAssert("placeholder shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.EqualTo(1));
} }
private void createResults(Func<ScoreInfo>? getScore = null) private void createResultsWithScore(Func<ScoreInfo> getScore)
{ {
AddStep("load results", () => AddStep("load results", () =>
{ {
LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) LoadScreen(resultsScreen = new TestScoreResultsScreen(getScore(), 1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{ {
RulesetID = new OsuRuleset().RulesetInfo.OnlineID RulesetID = new OsuRuleset().RulesetInfo.OnlineID
})); }));
@ -215,14 +245,27 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded); AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded);
} }
private void createUserBestResults()
{
AddStep("load results", () =>
{
LoadScreen(resultsScreen = new TestUserBestResultsScreen(1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}, 2));
});
AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded);
}
private void waitForDisplay() private void waitForDisplay()
{ {
AddUntilStep("wait for scores loaded", () => AddUntilStep("wait for scores loaded", () =>
requestComplete requestComplete
// request handler may need to fire more than once to get scores. // request handler may need to fire more than once to get scores.
&& totalCount > 0 && totalCount > 0
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount && resultsScreen.ChildrenOfType<ScorePanelList>().Single().GetScorePanels().Count() == totalCount
&& resultsScreen.ScorePanelList.AllPanelsVisible); && resultsScreen.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddWaitStep("wait for display", 5); AddWaitStep("wait for display", 5);
} }
@ -231,6 +274,7 @@ namespace osu.Game.Tests.Visual.Playlists
// pre-check for requests we should be handling (as they are scheduled below). // pre-check for requests we should be handling (as they are scheduled below).
switch (request) switch (request)
{ {
case ShowPlaylistScoreRequest:
case ShowPlaylistUserScoreRequest: case ShowPlaylistUserScoreRequest:
case IndexPlaylistScoresRequest: case IndexPlaylistScoresRequest:
break; break;
@ -253,7 +297,7 @@ namespace osu.Game.Tests.Visual.Playlists
switch (request) switch (request)
{ {
case ShowPlaylistUserScoreRequest s: case ShowPlaylistScoreRequest s:
if (userScore == null) if (userScore == null)
triggerFail(s); triggerFail(s);
else else
@ -261,6 +305,14 @@ namespace osu.Game.Tests.Visual.Playlists
break; break;
case ShowPlaylistUserScoreRequest u:
if (userScore == null)
triggerFail(u);
else
triggerSuccess(u, createUserResponse(userScore));
break;
case IndexPlaylistScoresRequest i: case IndexPlaylistScoresRequest i:
triggerSuccess(i, createIndexResponse(i, noScores)); triggerSuccess(i, createIndexResponse(i, noScores));
break; break;
@ -314,7 +366,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = userScore.MaxCombo, MaxCombo = userScore.MaxCombo,
User = new APIUser User = new APIUser
{ {
Id = 2, Id = 2 + i,
Username = $"peppy{i}", Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, },
@ -329,7 +381,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = userScore.MaxCombo, MaxCombo = userScore.MaxCombo,
User = new APIUser User = new APIUser
{ {
Id = 2, Id = 2 + i,
Username = $"peppy{i}", Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, },
@ -363,7 +415,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = 1000, MaxCombo = 1000,
User = new APIUser User = new APIUser
{ {
Id = 2, Id = 2 + i,
Username = $"peppy{i}", Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, },
@ -410,18 +462,22 @@ namespace osu.Game.Tests.Visual.Playlists
}; };
} }
private partial class TestResultsScreen : PlaylistItemUserResultsScreen private partial class TestScoreResultsScreen : PlaylistItemScoreResultsScreen
{ {
public new LoadingSpinner LeftSpinner => base.LeftSpinner; public TestScoreResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem)
public new LoadingSpinner CentreSpinner => base.CentreSpinner;
public new LoadingSpinner RightSpinner => base.RightSpinner;
public new ScorePanelList ScorePanelList => base.ScorePanelList;
public TestResultsScreen(ScoreInfo? score, int roomId, PlaylistItem playlistItem)
: base(score, roomId, playlistItem) : base(score, roomId, playlistItem)
{ {
AllowRetry = true; AllowRetry = true;
} }
} }
private partial class TestUserBestResultsScreen : PlaylistItemUserBestResultsScreen
{
public TestUserBestResultsScreen(int roomId, PlaylistItem playlistItem, int userId)
: base(roomId, playlistItem, userId)
{
AllowRetry = true;
}
}
} }
} }

View File

@ -2,8 +2,13 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Online.Rooms.RoomStatuses;
@ -14,6 +19,9 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene
{ {
[Resolved]
private IAPIProvider api { get; set; } = null!;
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
[Test] [Test]
@ -37,5 +45,29 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen()); AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen());
AddAssert("status is still ended", () => roomScreen.Room.Status, Is.TypeOf<RoomStatusEnded>); AddAssert("status is still ended", () => roomScreen.Room.Status, Is.TypeOf<RoomStatusEnded>);
} }
[Test]
public void TestCloseButtonGoesAwayAfterGracePeriod()
{
Room room = null!;
PlaylistsRoomSubScreen roomScreen = null!;
AddStep("create room", () =>
{
RoomManager.AddRoom(room = new Room
{
Name = @"Test Room",
Host = api.LocalUser.Value,
Category = RoomCategory.Normal,
StartDate = DateTimeOffset.Now.AddMinutes(-5).AddSeconds(3),
EndDate = DateTimeOffset.Now.AddMinutes(30)
});
});
AddStep("push screen", () => LoadScreen(roomScreen = new PlaylistsRoomSubScreen(room)));
AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen());
AddAssert("close button present", () => roomScreen.ChildrenOfType<DangerousRoundedButton>().Any());
AddUntilStep("wait for close button to disappear", () => !roomScreen.ChildrenOfType<DangerousRoundedButton>().Any());
}
} }
} }

View File

@ -112,6 +112,6 @@ namespace osu.Game.Tests.Visual.Ranking
}); });
private void displayUpdate(UserStatistics before, UserStatistics after) => private void displayUpdate(UserStatistics before, UserStatistics after) =>
AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new UserStatisticsUpdate(new ScoreInfo(), before, after)); AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new ScoreBasedUserStatisticsUpdate(new ScoreInfo(), before, after));
} }
} }

View File

@ -91,12 +91,12 @@ namespace osu.Game.Tests.Visual.Ranking
UserStatisticsWatcher userStatisticsWatcher = null!; UserStatisticsWatcher userStatisticsWatcher = null!;
ScoreInfo score = null!; ScoreInfo score = null!;
AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher())); AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher(new LocalUserStatisticsProvider())));
AddStep("set user statistics update", () => AddStep("set user statistics update", () =>
{ {
score = TestResources.CreateTestScoreInfo(); score = TestResources.CreateTestScoreInfo();
score.OnlineID = 1234; score.OnlineID = 1234;
((Bindable<UserStatisticsUpdate>)userStatisticsWatcher.LatestUpdate).Value = new UserStatisticsUpdate(score, ((Bindable<ScoreBasedUserStatisticsUpdate>)userStatisticsWatcher.LatestUpdate).Value = new ScoreBasedUserStatisticsUpdate(score,
new UserStatistics new UserStatistics
{ {
Level = new UserStatistics.LevelInfo Level = new UserStatistics.LevelInfo
@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Ranking
Score = { Value = score }, Score = { Value = score },
DisplayedUserStatisticsUpdate = DisplayedUserStatisticsUpdate =
{ {
Value = new UserStatisticsUpdate(score, new UserStatistics Value = new ScoreBasedUserStatisticsUpdate(score, new UserStatistics
{ {
Level = new UserStatistics.LevelInfo Level = new UserStatistics.LevelInfo
{ {

View File

@ -8,14 +8,14 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania;
@ -28,25 +28,31 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
public partial class TestSceneBeatmapRecommendations : OsuGameTestScene public partial class TestSceneBeatmapRecommendations : OsuGameTestScene
{ {
[Resolved]
private IRulesetStore rulesetStore { get; set; }
[SetUpSteps] [SetUpSteps]
public override void SetUpSteps() public override void SetUpSteps()
{ {
AddStep("populate ruleset statistics", () => AddStep("populate ruleset statistics", () =>
{ {
Dictionary<string, UserStatistics> rulesetStatistics = new Dictionary<string, UserStatistics>(); ((DummyAPIAccess)API).HandleRequest = r =>
rulesetStore.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo =>
{ {
rulesetStatistics[rulesetInfo.ShortName] = new UserStatistics switch (r)
{ {
PP = getNecessaryPP(rulesetInfo.OnlineID) case GetUserRequest userRequest:
}; userRequest.TriggerSuccess(new APIUser
}); {
Id = 99,
Statistics = new UserStatistics
{
PP = getNecessaryPP(userRequest.Ruleset?.OnlineID ?? 0)
}
});
API.LocalUser.Value.RulesetsStatistics = rulesetStatistics; return true;
default:
return false;
}
};
}); });
decimal getNecessaryPP(int? rulesetID) decimal getNecessaryPP(int? rulesetID)

View File

@ -408,7 +408,7 @@ namespace osu.Game.Beatmaps
// user requested abort // user requested abort
return; return;
var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.OrdinalIgnoreCase))); var video = b.Files.FirstOrDefault(f => SupportedExtensions.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.OrdinalIgnoreCase)));
if (video != null) if (video != null)
{ {
@ -559,7 +559,11 @@ namespace osu.Game.Beatmaps
// If we seem to be missing files, now is a good time to re-fetch. // If we seem to be missing files, now is a good time to re-fetch.
bool missingFiles = beatmapInfo.BeatmapSet?.Files.Count == 0; bool missingFiles = beatmapInfo.BeatmapSet?.Files.Count == 0;
if (refetch || beatmapInfo.IsManaged || missingFiles) if (beatmapInfo.IsManaged)
{
beatmapInfo = beatmapInfo.Detach();
}
else if (refetch || missingFiles)
{ {
Guid id = beatmapInfo.ID; Guid id = beatmapInfo.ID;
beatmapInfo = Realm.Run(r => r.Find<BeatmapInfo>(id)?.Detach()) ?? beatmapInfo; beatmapInfo = Realm.Run(r => r.Find<BeatmapInfo>(id)?.Detach()) ?? beatmapInfo;

View File

@ -9,9 +9,11 @@ using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Online.API; using osu.Game.Online;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Users;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
@ -21,18 +23,63 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
public partial class DifficultyRecommender : Component public partial class DifficultyRecommender : Component
{ {
[Resolved] private readonly LocalUserStatisticsProvider statisticsProvider;
private IAPIProvider api { get; set; }
[Resolved] [Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } private Bindable<RulesetInfo> gameRuleset { get; set; }
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
private readonly Dictionary<string, double> recommendedDifficultyMapping = new Dictionary<string, double>(); private readonly Dictionary<string, double> recommendedDifficultyMapping = new Dictionary<string, double>();
/// <returns>
/// Rulesets ordered descending by their respective recommended difficulties.
/// The currently selected ruleset will always be first.
/// </returns>
private IEnumerable<string> orderedRulesets
{
get
{
if (LoadState < LoadState.Ready || gameRuleset.Value == null)
return Enumerable.Empty<string>();
return recommendedDifficultyMapping
.OrderByDescending(pair => pair.Value)
.Select(pair => pair.Key)
.Where(r => !r.Equals(gameRuleset.Value.ShortName, StringComparison.Ordinal))
.Prepend(gameRuleset.Value.ShortName);
}
}
public DifficultyRecommender(LocalUserStatisticsProvider statisticsProvider)
{
this.statisticsProvider = statisticsProvider;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
api.LocalUser.BindValueChanged(_ => populateValues(), true); foreach (var ruleset in rulesets.AvailableRulesets)
{
if (statisticsProvider.GetStatisticsFor(ruleset) is UserStatistics statistics)
updateMapping(ruleset, statistics);
}
}
protected override void LoadComplete()
{
base.LoadComplete();
statisticsProvider.StatisticsUpdated += onStatisticsUpdated;
}
private void onStatisticsUpdated(UserStatisticsUpdate update) => updateMapping(update.Ruleset, update.NewStatistics);
private void updateMapping(RulesetInfo ruleset, UserStatistics statistics)
{
// algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
recommendedDifficultyMapping[ruleset.ShortName] = Math.Pow((double)(statistics.PP ?? 0), 0.4) * 0.195;
} }
/// <summary> /// <summary>
@ -64,35 +111,12 @@ namespace osu.Game.Beatmaps
return null; return null;
} }
private void populateValues() protected override void Dispose(bool isDisposing)
{ {
if (api.LocalUser.Value.RulesetsStatistics == null) if (statisticsProvider.IsNotNull())
return; statisticsProvider.StatisticsUpdated -= onStatisticsUpdated;
foreach (var kvp in api.LocalUser.Value.RulesetsStatistics) base.Dispose(isDisposing);
{
// algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
recommendedDifficultyMapping[kvp.Key] = Math.Pow((double)(kvp.Value.PP ?? 0), 0.4) * 0.195;
}
}
/// <returns>
/// Rulesets ordered descending by their respective recommended difficulties.
/// The currently selected ruleset will always be first.
/// </returns>
private IEnumerable<string> orderedRulesets
{
get
{
if (LoadState < LoadState.Ready || ruleset.Value == null)
return Enumerable.Empty<string>();
return recommendedDifficultyMapping
.OrderByDescending(pair => pair.Value)
.Select(pair => pair.Key)
.Where(r => !r.Equals(ruleset.Value.ShortName, StringComparison.Ordinal))
.Prepend(ruleset.Value.ShortName);
}
} }
} }
} }

View File

@ -17,6 +17,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Utils;
namespace osu.Game.Beatmaps.Formats namespace osu.Game.Beatmaps.Formats
{ {
@ -446,7 +447,7 @@ namespace osu.Game.Beatmaps.Formats
// Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO // Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO
// instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported // instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported
// video extensions and handle similar to a background if it doesn't match. // video extensions and handle similar to a background if it doesn't match.
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant())) if (!SupportedExtensions.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant()))
{ {
beatmap.BeatmapInfo.Metadata.BackgroundFile = filename; beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
lineSupportedByEncoder = true; lineSupportedByEncoder = true;

View File

@ -10,6 +10,7 @@ using osu.Game.Beatmaps.Legacy;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Storyboards.Commands; using osu.Game.Storyboards.Commands;
using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -112,7 +113,7 @@ namespace osu.Game.Beatmaps.Formats
// //
// This avoids potential weird crashes when ffmpeg attempts to parse an image file as a video // This avoids potential weird crashes when ffmpeg attempts to parse an image file as a video
// (see https://github.com/ppy/osu/issues/22829#issuecomment-1465552451). // (see https://github.com/ppy/osu/issues/22829#issuecomment-1465552451).
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant())) if (!SupportedExtensions.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant()))
break; break;
storyboard.GetLayer("Video").Add(storyboardSprite = new StoryboardVideo(path, offset)); storyboard.GetLayer("Video").Add(storyboardSprite = new StoryboardVideo(path, offset));

View File

@ -138,6 +138,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.LightenDuringBreaks, true); SetDefault(OsuSetting.LightenDuringBreaks, true);
SetDefault(OsuSetting.HitLighting, true); SetDefault(OsuSetting.HitLighting, true);
SetDefault(OsuSetting.StarFountains, true);
SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always); SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always);
SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true); SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true);
@ -414,6 +415,7 @@ namespace osu.Game.Configuration
NotifyOnPrivateMessage, NotifyOnPrivateMessage,
UIHoldActivationDelay, UIHoldActivationDelay,
HitLighting, HitLighting,
StarFountains,
MenuBackgroundSource, MenuBackgroundSource,
GameplayDisableWinKey, GameplayDisableWinKey,
SeasonalBackgroundMode, SeasonalBackgroundMode,

View File

@ -10,6 +10,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
@ -77,7 +78,8 @@ namespace osu.Game.Configuration
TouchInputActive, TouchInputActive,
/// <summary> /// <summary>
/// Stores the local user's last score (can be completed or aborted). /// Contains the local user's last score (can be completed or aborted) after exiting <see cref="Player"/>.
/// Will be cleared to <c>null</c> when leaving <see cref="PlayerLoader"/>.
/// </summary> /// </summary>
LastLocalUserScore, LastLocalUserScore,

View File

@ -245,8 +245,8 @@ namespace osu.Game.Database
var scoreProcessor = ruleset.CreateScoreProcessor(); var scoreProcessor = ruleset.CreateScoreProcessor();
// warning: ordering is important here - both total score and ranks are dependent on accuracy! // warning: ordering is important here - both total score and ranks are dependent on accuracy!
score.Accuracy = computeAccuracy(score, scoreProcessor); score.Accuracy = ComputeAccuracy(score, scoreProcessor);
score.Rank = computeRank(score, scoreProcessor); score.Rank = ComputeRank(score, scoreProcessor);
(score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, beatmap); (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, beatmap);
} }
@ -269,8 +269,8 @@ namespace osu.Game.Database
var scoreProcessor = ruleset.CreateScoreProcessor(); var scoreProcessor = ruleset.CreateScoreProcessor();
// warning: ordering is important here - both total score and ranks are dependent on accuracy! // warning: ordering is important here - both total score and ranks are dependent on accuracy!
score.Accuracy = computeAccuracy(score, scoreProcessor); score.Accuracy = ComputeAccuracy(score, scoreProcessor);
score.Rank = computeRank(score, scoreProcessor); score.Rank = ComputeRank(score, scoreProcessor);
(score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes);
} }
@ -313,7 +313,8 @@ namespace osu.Game.Database
/// <param name="difficulty">The beatmap difficulty.</param> /// <param name="difficulty">The beatmap difficulty.</param>
/// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param> /// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param>
/// <returns>The standardised total score.</returns> /// <returns>The standardised total score.</returns>
private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty,
LegacyScoreAttributes attributes)
{ {
if (!score.IsLegacyScore) if (!score.IsLegacyScore)
return (score.TotalScoreWithoutMods, score.TotalScore); return (score.TotalScoreWithoutMods, score.TotalScore);
@ -620,24 +621,31 @@ namespace osu.Game.Database
} }
} }
private static double computeAccuracy(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) public static double ComputeAccuracy(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor)
=> ComputeAccuracy(scoreInfo.Statistics, scoreInfo.MaximumStatistics, scoreProcessor);
public static double ComputeAccuracy(IReadOnlyDictionary<HitResult, int> statistics, IReadOnlyDictionary<HitResult, int> maximumStatistics, ScoreProcessor scoreProcessor)
{ {
int baseScore = scoreInfo.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()) int baseScore = statistics.Where(kvp => kvp.Key.AffectsAccuracy())
.Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key)); .Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key));
int maxBaseScore = scoreInfo.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()) int maxBaseScore = maximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy())
.Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key)); .Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key));
return maxBaseScore == 0 ? 1 : baseScore / (double)maxBaseScore; return maxBaseScore == 0 ? 1 : baseScore / (double)maxBaseScore;
} }
public static ScoreRank ComputeRank(ScoreInfo scoreInfo) => computeRank(scoreInfo, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor()); public static ScoreRank ComputeRank(ScoreInfo scoreInfo) =>
ComputeRank(scoreInfo.Accuracy, scoreInfo.Statistics, scoreInfo.Mods, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor());
private static ScoreRank computeRank(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) public static ScoreRank ComputeRank(ScoreInfo scoreInfo, ScoreProcessor processor) =>
ComputeRank(scoreInfo.Accuracy, scoreInfo.Statistics, scoreInfo.Mods, processor);
public static ScoreRank ComputeRank(double accuracy, IReadOnlyDictionary<HitResult, int> statistics, IList<Mod> mods, ScoreProcessor scoreProcessor)
{ {
var rank = scoreProcessor.RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics); var rank = scoreProcessor.RankFromScore(accuracy, statistics);
foreach (var mod in scoreInfo.Mods.OfType<IApplicableToScoreProcessor>()) foreach (var mod in mods.OfType<IApplicableToScoreProcessor>())
rank = mod.AdjustRank(rank, scoreInfo.Accuracy); rank = mod.AdjustRank(rank, accuracy);
return rank; return rank;
} }

View File

@ -13,6 +13,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2.FileSelection; using osu.Game.Graphics.UserInterfaceV2.FileSelection;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Utils;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
@ -96,24 +97,18 @@ namespace osu.Game.Graphics.UserInterfaceV2
{ {
get get
{ {
if (OsuGameBase.VIDEO_EXTENSIONS.Contains(File.Extension.ToLowerInvariant())) string extension = File.Extension.ToLowerInvariant();
if (SupportedExtensions.VIDEO_EXTENSIONS.Contains(extension))
return FontAwesome.Regular.FileVideo; return FontAwesome.Regular.FileVideo;
switch (File.Extension) if (SupportedExtensions.AUDIO_EXTENSIONS.Contains(extension))
{ return FontAwesome.Regular.FileAudio;
case @".ogg":
case @".mp3":
case @".wav":
return FontAwesome.Regular.FileAudio;
case @".jpg": if (SupportedExtensions.IMAGE_EXTENSIONS.Contains(extension))
case @".jpeg": return FontAwesome.Regular.FileImage;
case @".png":
return FontAwesome.Regular.FileImage;
default: return FontAwesome.Regular.File;
return FontAwesome.Regular.File;
}
} }
} }

View File

@ -74,6 +74,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString FadePlayfieldWhenHealthLow => new TranslatableString(getKey(@"fade_playfield_when_health_low"), @"Fade playfield to red when health is low"); public static LocalisableString FadePlayfieldWhenHealthLow => new TranslatableString(getKey(@"fade_playfield_when_health_low"), @"Fade playfield to red when health is low");
/// <summary>
/// "Star fountains"
/// </summary>
public static LocalisableString StarFountains => new TranslatableString(getKey(@"star_fountains"), @"Star fountains");
/// <summary> /// <summary>
/// "Always show key overlay" /// "Always show key overlay"
/// </summary> /// </summary>

View File

@ -0,0 +1,154 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class MenuTipStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.MenuTip";
/// <summary>
/// "Press Ctrl-T anywhere in the game to toggle the toolbar!"
/// </summary>
public static LocalisableString ToggleToolbarShortcut => new TranslatableString(getKey(@"toggle_toolbar_shortcut"), @"Press Ctrl-T anywhere in the game to toggle the toolbar!");
/// <summary>
/// "Press Ctrl-O anywhere in the game to access settings!"
/// </summary>
public static LocalisableString GameSettingsShortcut => new TranslatableString(getKey(@"game_settings_shortcut"), @"Press Ctrl-O anywhere in the game to access settings!");
/// <summary>
/// "All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!"
/// </summary>
public static LocalisableString DynamicSettings => new TranslatableString(getKey(@"dynamic_settings"), @"All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!");
/// <summary>
/// "New features are coming online every update. Make sure to stay up-to-date!"
/// </summary>
public static LocalisableString NewFeaturesAreComingOnline => new TranslatableString(getKey(@"new_features_are_coming_online"), @"New features are coming online every update. Make sure to stay up-to-date!");
/// <summary>
/// "If you find the UI too large or small, try adjusting UI scale in settings!"
/// </summary>
public static LocalisableString UIScalingSettings => new TranslatableString(getKey(@"ui_scaling_settings"), @"If you find the UI too large or small, try adjusting UI scale in settings!");
/// <summary>
/// "Try adjusting the &quot;Screen Scaling&quot; mode to change your gameplay or UI area, even in fullscreen!"
/// </summary>
public static LocalisableString ScreenScalingSettings => new TranslatableString(getKey(@"screen_scaling_settings"), @"Try adjusting the ""Screen Scaling"" mode to change your gameplay or UI area, even in fullscreen!");
/// <summary>
/// "What used to be &quot;osu!direct&quot; is available to all users just like on the website. You can access it anywhere using Ctrl-B!"
/// </summary>
public static LocalisableString FreeOsuDirect => new TranslatableString(getKey(@"free_osu_direct"), @"What used to be ""osu!direct"" is available to all users just like on the website. You can access it anywhere using Ctrl-B!");
/// <summary>
/// "Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!"
/// </summary>
public static LocalisableString ReplaySeeking => new TranslatableString(getKey(@"replay_seeking"), @"Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!");
/// <summary>
/// "Try scrolling right in mod select to find a bunch of new fun mods!"
/// </summary>
public static LocalisableString TryNewMods => new TranslatableString(getKey(@"try_new_mods"), @"Try scrolling right in mod select to find a bunch of new fun mods!");
/// <summary>
/// "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!"
/// </summary>
public static LocalisableString EmbeddedWebContent => new TranslatableString(getKey(@"embedded_web_content"), @"Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!");
/// <summary>
/// "Get more details, hide or delete a beatmap by right-clicking on its panel at song select!"
/// </summary>
public static LocalisableString BeatmapRightClick => new TranslatableString(getKey(@"beatmap_right_click"), @"Get more details, hide or delete a beatmap by right-clicking on its panel at song select!");
/// <summary>
/// "Check out the &quot;playlists&quot; system, which lets users create their own custom and permanent leaderboards!"
/// </summary>
public static LocalisableString DiscoverPlaylists => new TranslatableString(getKey(@"discover_playlists"), @"Check out the ""playlists"" system, which lets users create their own custom and permanent leaderboards!");
/// <summary>
/// "Toggle advanced frame / thread statistics with Ctrl-F11!"
/// </summary>
public static LocalisableString ToggleAdvancedFPSCounter => new TranslatableString(getKey(@"toggle_advanced_fps_counter"), @"Toggle advanced frame / thread statistics with Ctrl-F11!");
/// <summary>
/// "You can pause during a replay by pressing Space!"
/// </summary>
public static LocalisableString ReplayPausing => new TranslatableString(getKey(@"replay_pausing"), @"You can pause during a replay by pressing Space!");
/// <summary>
/// "Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!"
/// </summary>
public static LocalisableString ConfigurableHotkeys => new TranslatableString(getKey(@"configurable_hotkeys"), @"Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!");
/// <summary>
/// "Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via Ctrl-Shift-S!"
/// </summary>
public static LocalisableString SkinEditor => new TranslatableString(getKey(@"skin_editor"), @"Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via Ctrl-Shift-S!");
/// <summary>
/// "You can create mod presets to make toggling your favourite mod combinations easier!"
/// </summary>
public static LocalisableString ModPresets => new TranslatableString(getKey(@"mod_presets"), @"You can create mod presets to make toggling your favourite mod combinations easier!");
/// <summary>
/// "Many mods have customisation settings that drastically change how they function. Click the Customise button in mod select to view settings!"
/// </summary>
public static LocalisableString ModCustomisationSettings => new TranslatableString(getKey(@"mod_customisation_settings"), @"Many mods have customisation settings that drastically change how they function. Click the Customise button in mod select to view settings!");
/// <summary>
/// "Press Ctrl-Shift-R to switch to a random skin!"
/// </summary>
public static LocalisableString RandomSkinShortcut => new TranslatableString(getKey(@"random_skin_shortcut"), @"Press Ctrl-Shift-R to switch to a random skin!");
/// <summary>
/// "While watching a replay, press Ctrl-H to toggle replay settings!"
/// </summary>
public static LocalisableString ToggleReplaySettingsShortcut => new TranslatableString(getKey(@"toggle_replay_settings_shortcut"), @"While watching a replay, press Ctrl-H to toggle replay settings!");
/// <summary>
/// "You can easily copy the mods from scores on a leaderboard by right-clicking on them!"
/// </summary>
public static LocalisableString CopyModsFromScore => new TranslatableString(getKey(@"copy_mods_from_score"), @"You can easily copy the mods from scores on a leaderboard by right-clicking on them!");
/// <summary>
/// "Ctrl-Enter at song select will start a beatmap in autoplay mode!"
/// </summary>
public static LocalisableString AutoplayBeatmapShortcut => new TranslatableString(getKey(@"autoplay_beatmap_shortcut"), @"Ctrl-Enter at song select will start a beatmap in autoplay mode!");
/// <summary>
/// "Multithreading support means that even with low &quot;FPS&quot; your input and judgements will be accurate!"
/// </summary>
public static LocalisableString MultithreadingSupport => new TranslatableString(getKey(@"multithreading_support"), @"Multithreading support means that even with low ""FPS"" your input and judgements will be accurate!");
/// <summary>
/// "All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!"
/// </summary>
public static LocalisableString TemporaryDeleteOperations => new TranslatableString(getKey(@"temporary_delete_operations"), @"All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!");
/// <summary>
/// "Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!"
/// </summary>
public static LocalisableString GlobalStatisticsShortcut => new TranslatableString(getKey(@"global_statistics_shortcut"), @"Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!");
/// <summary>
/// "When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!"
/// </summary>
public static LocalisableString PeekHUDWhenHidden => new TranslatableString(getKey(@"peek_hud_when_hidden"), @"When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!");
/// <summary>
/// "Drag and drop any image into the skin editor to load it in quickly!"
/// </summary>
public static LocalisableString DragAndDropImageInSkinEditor => new TranslatableString(getKey(@"drag_and_drop_image_in_skin_editor"), @"Drag and drop any image into the skin editor to load it in quickly!");
/// <summary>
/// "a tip for you:"
/// </summary>
public static LocalisableString MenuTipTitle => new TranslatableString(getKey(@"menu_tip_title"), @"a tip for you:");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -59,7 +59,6 @@ namespace osu.Game.Online.API
public IBindable<APIUser> LocalUser => localUser; public IBindable<APIUser> LocalUser => localUser;
public IBindableList<APIRelation> Friends => friends; public IBindableList<APIRelation> Friends => friends;
public IBindable<UserActivity> Activity => activity; public IBindable<UserActivity> Activity => activity;
public IBindable<UserStatistics> Statistics => statistics;
public INotificationsClient NotificationsClient { get; } public INotificationsClient NotificationsClient { get; }
@ -74,8 +73,6 @@ namespace osu.Game.Online.API
private Bindable<UserStatus?> configStatus { get; } = new Bindable<UserStatus?>(); private Bindable<UserStatus?> configStatus { get; } = new Bindable<UserStatus?>();
private Bindable<UserStatus?> localUserStatus { get; } = new Bindable<UserStatus?>(); private Bindable<UserStatus?> localUserStatus { get; } = new Bindable<UserStatus?>();
private Bindable<UserStatistics> statistics { get; } = new Bindable<UserStatistics>();
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
@ -604,14 +601,6 @@ namespace osu.Game.Online.API
flushQueue(); flushQueue();
} }
public void UpdateStatistics(UserStatistics newStatistics)
{
statistics.Value = newStatistics;
if (IsLoggedIn)
localUser.Value.Statistics = newStatistics;
}
public void UpdateLocalFriends() public void UpdateLocalFriends()
{ {
if (!IsLoggedIn) if (!IsLoggedIn)
@ -630,11 +619,7 @@ namespace osu.Game.Online.API
private static APIUser createGuestUser() => new GuestUser(); private static APIUser createGuestUser() => new GuestUser();
private void setLocalUser(APIUser user) => Scheduler.Add(() => private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false);
{
localUser.Value = user;
statistics.Value = user.Statistics;
}, false);
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {

View File

@ -30,8 +30,6 @@ namespace osu.Game.Online.API
public Bindable<UserActivity> Activity { get; } = new Bindable<UserActivity>(); public Bindable<UserActivity> Activity { get; } = new Bindable<UserActivity>();
public Bindable<UserStatistics?> Statistics { get; } = new Bindable<UserStatistics?>();
public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient(); public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient();
INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient; INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient;
@ -178,11 +176,6 @@ namespace osu.Game.Online.API
private void onSuccessfulLogin() private void onSuccessfulLogin()
{ {
state.Value = APIState.Online; state.Value = APIState.Online;
Statistics.Value = new UserStatistics
{
GlobalRank = 1,
CountryRank = 1
};
} }
public void Logout() public void Logout()
@ -193,14 +186,6 @@ namespace osu.Game.Online.API
LocalUser.Value = new GuestUser(); LocalUser.Value = new GuestUser();
} }
public void UpdateStatistics(UserStatistics newStatistics)
{
Statistics.Value = newStatistics;
if (IsLoggedIn)
LocalUser.Value.Statistics = newStatistics;
}
public void UpdateLocalFriends() public void UpdateLocalFriends()
{ {
} }
@ -220,7 +205,6 @@ namespace osu.Game.Online.API
IBindable<APIUser> IAPIProvider.LocalUser => LocalUser; IBindable<APIUser> IAPIProvider.LocalUser => LocalUser;
IBindableList<APIRelation> IAPIProvider.Friends => Friends; IBindableList<APIRelation> IAPIProvider.Friends => Friends;
IBindable<UserActivity> IAPIProvider.Activity => Activity; IBindable<UserActivity> IAPIProvider.Activity => Activity;
IBindable<UserStatistics?> IAPIProvider.Statistics => Statistics;
/// <summary> /// <summary>
/// Skip 2FA requirement for next login. /// Skip 2FA requirement for next login.

View File

@ -29,11 +29,6 @@ namespace osu.Game.Online.API
/// </summary> /// </summary>
IBindable<UserActivity> Activity { get; } IBindable<UserActivity> Activity { get; }
/// <summary>
/// The current user's online statistics.
/// </summary>
IBindable<UserStatistics?> Statistics { get; }
/// <summary> /// <summary>
/// The language supplied by this provider to API requests. /// The language supplied by this provider to API requests.
/// </summary> /// </summary>
@ -129,11 +124,6 @@ namespace osu.Game.Online.API
/// </summary> /// </summary>
void Logout(); void Logout();
/// <summary>
/// Sets Statistics bindable.
/// </summary>
void UpdateStatistics(UserStatistics newStatistics);
/// <summary> /// <summary>
/// Update the friends status of the current user. /// Update the friends status of the current user.
/// </summary> /// </summary>

View File

@ -0,0 +1,27 @@
// 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.Net.Http;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API.Requests
{
public class ClosePlaylistRequest : APIRequest
{
private readonly long roomId;
public ClosePlaylistRequest(long roomId)
{
this.roomId = roomId;
}
protected override WebRequest CreateWebRequest()
{
var request = base.CreateWebRequest();
request.Method = HttpMethod.Delete;
return request;
}
protected override string Target => $@"rooms/{roomId}";
}
}

View File

@ -223,8 +223,10 @@ namespace osu.Game.Online.API.Requests.Responses
/// <summary> /// <summary>
/// User statistics for the requested ruleset (in the case of a <see cref="GetUserRequest"/> or <see cref="GetFriendsRequest"/> response). /// User statistics for the requested ruleset (in the case of a <see cref="GetUserRequest"/> or <see cref="GetFriendsRequest"/> response).
/// Otherwise empty.
/// </summary> /// </summary>
/// <remarks>
/// This returns null when accessed from <see cref="IAPIProvider.LocalUser"/>. Use <see cref="LocalUserStatisticsProvider"/> instead.
/// </remarks>
[JsonProperty(@"statistics")] [JsonProperty(@"statistics")]
public UserStatistics Statistics public UserStatistics Statistics
{ {

View File

@ -161,7 +161,7 @@ namespace osu.Game.Online.Chat
Messages.AddRange(messages); Messages.AddRange(messages);
long? maxMessageId = messages.Max(m => m.Id); long? maxMessageId = messages.Max(m => m.Id);
if (maxMessageId > LastMessageId) if (LastMessageId == null || maxMessageId > LastMessageId)
LastMessageId = maxMessageId; LastMessageId = maxMessageId;
purgeOldMessages(); purgeOldMessages();

View File

@ -0,0 +1,92 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using osu.Game.Users;
namespace osu.Game.Online
{
/// <summary>
/// A component that keeps track of the latest statistics for the local user.
/// </summary>
public partial class LocalUserStatisticsProvider : Component
{
/// <summary>
/// Invoked whenever a change occured to the statistics of any ruleset,
/// either due to change in local user (log out and log in) or as a result of score submission.
/// </summary>
/// <remarks>
/// This does not guarantee the presence of the old statistics,
/// specifically in the case of initial population or change in local user.
/// </remarks>
public event Action<UserStatisticsUpdate>? StatisticsUpdated;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly Dictionary<string, UserStatistics> statisticsCache = new Dictionary<string, UserStatistics>();
/// <summary>
/// Returns the <see cref="UserStatistics"/> currently available for the given ruleset.
/// This may return null if the requested statistics has not been fetched before yet.
/// </summary>
/// <param name="ruleset">The ruleset to return the corresponding <see cref="UserStatistics"/> for.</param>
public UserStatistics? GetStatisticsFor(RulesetInfo ruleset) => statisticsCache.GetValueOrDefault(ruleset.ShortName);
protected override void LoadComplete()
{
base.LoadComplete();
api.LocalUser.BindValueChanged(_ =>
{
// queuing up requests directly on user change is unsafe, as the API status may have not been updated yet.
// schedule a frame to allow the API to be in its correct state sending requests.
Schedule(initialiseStatistics);
}, true);
}
private void initialiseStatistics()
{
statisticsCache.Clear();
if (api.LocalUser.Value == null || api.LocalUser.Value.Id <= 1)
return;
foreach (var ruleset in rulesets.AvailableRulesets.Where(r => r.IsLegacyRuleset()))
RefetchStatistics(ruleset);
}
public void RefetchStatistics(RulesetInfo ruleset, Action<UserStatisticsUpdate>? callback = null)
{
if (!ruleset.IsLegacyRuleset())
throw new InvalidOperationException($@"Retrieving statistics is not supported for ruleset {ruleset.ShortName}");
var request = new GetUserRequest(api.LocalUser.Value.Id, ruleset);
request.Success += u => UpdateStatistics(u.Statistics, ruleset, callback);
api.Queue(request);
}
protected void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset, Action<UserStatisticsUpdate>? callback = null)
{
var oldStatistics = statisticsCache.GetValueOrDefault(ruleset.ShortName);
statisticsCache[ruleset.ShortName] = newStatistics;
var update = new UserStatisticsUpdate(ruleset, oldStatistics, newStatistics);
callback?.Invoke(update);
StatisticsUpdated?.Invoke(update);
}
}
public record UserStatisticsUpdate(RulesetInfo Ruleset, UserStatistics? OldStatistics, UserStatistics NewStatistics);
}

View File

@ -375,6 +375,7 @@ namespace osu.Game.Online.Rooms
Type = other.Type; Type = other.Type;
MaxParticipants = other.MaxParticipants; MaxParticipants = other.MaxParticipants;
ParticipantCount = other.ParticipantCount; ParticipantCount = other.ParticipantCount;
StartDate = other.StartDate;
EndDate = other.EndDate; EndDate = other.EndDate;
UserScore = other.UserScore; UserScore = other.UserScore;
QueueMode = other.QueueMode; QueueMode = other.QueueMode;

View File

@ -9,7 +9,7 @@ namespace osu.Game.Online
/// <summary> /// <summary>
/// Contains data about the change in a user's profile statistics after completing a score. /// Contains data about the change in a user's profile statistics after completing a score.
/// </summary> /// </summary>
public class UserStatisticsUpdate public class ScoreBasedUserStatisticsUpdate
{ {
/// <summary> /// <summary>
/// The score set by the user that triggered the update. /// The score set by the user that triggered the update.
@ -27,12 +27,12 @@ namespace osu.Game.Online
public UserStatistics After { get; } public UserStatistics After { get; }
/// <summary> /// <summary>
/// Creates a new <see cref="UserStatisticsUpdate"/>. /// Creates a new <see cref="ScoreBasedUserStatisticsUpdate"/>.
/// </summary> /// </summary>
/// <param name="score">The score set by the user that triggered the update.</param> /// <param name="score">The score set by the user that triggered the update.</param>
/// <param name="before">The user's profile statistics prior to the score being set.</param> /// <param name="before">The user's profile statistics prior to the score being set.</param>
/// <param name="after">The user's profile statistics after the score was set.</param> /// <param name="after">The user's profile statistics after the score was set.</param>
public UserStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after) public ScoreBasedUserStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after)
{ {
Score = score; Score = score;
Before = before; Before = before;

View File

@ -2,18 +2,14 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Online namespace osu.Game.Online
{ {
@ -22,8 +18,10 @@ namespace osu.Game.Online
/// </summary> /// </summary>
public partial class UserStatisticsWatcher : Component public partial class UserStatisticsWatcher : Component
{ {
public IBindable<UserStatisticsUpdate?> LatestUpdate => latestUpdate; private readonly LocalUserStatisticsProvider statisticsProvider;
private readonly Bindable<UserStatisticsUpdate?> latestUpdate = new Bindable<UserStatisticsUpdate?>();
public IBindable<ScoreBasedUserStatisticsUpdate?> LatestUpdate => latestUpdate;
private readonly Bindable<ScoreBasedUserStatisticsUpdate?> latestUpdate = new Bindable<ScoreBasedUserStatisticsUpdate?>();
[Resolved] [Resolved]
private SpectatorClient spectatorClient { get; set; } = null!; private SpectatorClient spectatorClient { get; set; } = null!;
@ -33,13 +31,15 @@ namespace osu.Game.Online
private readonly Dictionary<long, ScoreInfo> watchedScores = new Dictionary<long, ScoreInfo>(); private readonly Dictionary<long, ScoreInfo> watchedScores = new Dictionary<long, ScoreInfo>();
private Dictionary<string, UserStatistics>? latestStatistics; public UserStatisticsWatcher(LocalUserStatisticsProvider statisticsProvider)
{
this.statisticsProvider = statisticsProvider;
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
api.LocalUser.BindValueChanged(user => onUserChanged(user.NewValue), true);
spectatorClient.OnUserScoreProcessed += userScoreProcessed; spectatorClient.OnUserScoreProcessed += userScoreProcessed;
} }
@ -61,35 +61,6 @@ namespace osu.Game.Online
}); });
} }
private void onUserChanged(APIUser? localUser) => Schedule(() =>
{
latestStatistics = null;
if (localUser == null || localUser.OnlineID <= 1)
return;
var userRequest = new GetUsersRequest(new[] { localUser.OnlineID });
userRequest.Success += initialiseUserStatistics;
api.Queue(userRequest);
});
private void initialiseUserStatistics(GetUsersResponse response) => Schedule(() =>
{
var user = response.Users.SingleOrDefault();
// possible if the user is restricted or similar.
if (user == null)
return;
latestStatistics = new Dictionary<string, UserStatistics>();
if (user.RulesetsStatistics != null)
{
foreach (var rulesetStats in user.RulesetsStatistics)
latestStatistics.Add(rulesetStats.Key, rulesetStats.Value);
}
});
private void userScoreProcessed(int userId, long scoreId) private void userScoreProcessed(int userId, long scoreId)
{ {
if (userId != api.LocalUser.Value?.OnlineID) if (userId != api.LocalUser.Value?.OnlineID)
@ -98,30 +69,11 @@ namespace osu.Game.Online
if (!watchedScores.Remove(scoreId, out var scoreInfo)) if (!watchedScores.Remove(scoreId, out var scoreInfo))
return; return;
requestStatisticsUpdate(userId, scoreInfo); statisticsProvider.RefetchStatistics(scoreInfo.Ruleset, u => Schedule(() =>
} {
if (u.OldStatistics != null)
private void requestStatisticsUpdate(int userId, ScoreInfo scoreInfo) latestUpdate.Value = new ScoreBasedUserStatisticsUpdate(scoreInfo, u.OldStatistics, u.NewStatistics);
{ }));
var request = new GetUserRequest(userId, scoreInfo.Ruleset);
request.Success += user => Schedule(() => dispatchStatisticsUpdate(scoreInfo, user.Statistics));
api.Queue(request);
}
private void dispatchStatisticsUpdate(ScoreInfo scoreInfo, UserStatistics updatedStatistics)
{
string rulesetName = scoreInfo.Ruleset.ShortName;
api.UpdateStatistics(updatedStatistics);
if (latestStatistics == null)
return;
latestStatistics.TryGetValue(rulesetName, out UserStatistics? latestRulesetStatistics);
latestRulesetStatistics ??= new UserStatistics();
latestUpdate.Value = new UserStatisticsUpdate(scoreInfo, latestRulesetStatistics, updatedStatistics);
latestStatistics[rulesetName] = updatedStatistics;
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -148,8 +148,7 @@ namespace osu.Game
[Resolved] [Resolved]
private FrameworkConfigManager frameworkConfig { get; set; } private FrameworkConfigManager frameworkConfig { get; set; }
[Cached] private DifficultyRecommender difficultyRecommender;
private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender();
[Cached] [Cached]
private readonly LegacyImportManager legacyImportManager = new LegacyImportManager(); private readonly LegacyImportManager legacyImportManager = new LegacyImportManager();
@ -196,7 +195,8 @@ namespace osu.Game
private MainMenu menuScreen; private MainMenu menuScreen;
private VersionManager versionManager; [CanBeNull]
private DevBuildBanner devBuildBanner;
[CanBeNull] [CanBeNull]
private IntroScreen introScreen; private IntroScreen introScreen;
@ -1056,10 +1056,7 @@ namespace osu.Game
}, topMostOverlayContent.Add); }, topMostOverlayContent.Add);
if (!IsDeployedBuild) if (!IsDeployedBuild)
{ loadComponentSingleFile(devBuildBanner = new DevBuildBanner(), ScreenContainer.Add);
dependencies.Cache(versionManager = new VersionManager());
loadComponentSingleFile(versionManager, ScreenContainer.Add);
}
loadComponentSingleFile(osuLogo, _ => loadComponentSingleFile(osuLogo, _ =>
{ {
@ -1069,7 +1066,11 @@ namespace osu.Game
ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both)); ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both));
}); });
loadComponentSingleFile(new UserStatisticsWatcher(), Add, true); LocalUserStatisticsProvider statisticsProvider;
loadComponentSingleFile(statisticsProvider = new LocalUserStatisticsProvider(), Add, true);
loadComponentSingleFile(difficultyRecommender = new DifficultyRecommender(statisticsProvider), Add, true);
loadComponentSingleFile(new UserStatisticsWatcher(statisticsProvider), Add, true);
loadComponentSingleFile(Toolbar = new Toolbar loadComponentSingleFile(Toolbar = new Toolbar
{ {
OnHome = delegate OnHome = delegate
@ -1139,7 +1140,6 @@ namespace osu.Game
loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
loadComponentSingleFile(new DetachedBeatmapStore(), Add, true); loadComponentSingleFile(new DetachedBeatmapStore(), Add, true);
Add(difficultyRecommender);
Add(externalLinkOpener = new ExternalLinkOpener()); Add(externalLinkOpener = new ExternalLinkOpener());
Add(new MusicKeyBindingHandler()); Add(new MusicKeyBindingHandler());
Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen)); Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen));
@ -1562,12 +1562,12 @@ namespace osu.Game
{ {
case IntroScreen intro: case IntroScreen intro:
introScreen = intro; introScreen = intro;
versionManager?.Show(); devBuildBanner?.Show();
break; break;
case MainMenu menu: case MainMenu menu:
menuScreen = menu; menuScreen = menu;
versionManager?.Show(); devBuildBanner?.Show();
break; break;
case Player player: case Player player:
@ -1575,7 +1575,7 @@ namespace osu.Game
break; break;
default: default:
versionManager?.Hide(); devBuildBanner?.Hide();
break; break;
} }

View File

@ -73,8 +73,6 @@ namespace osu.Game
[Cached(typeof(OsuGameBase))] [Cached(typeof(OsuGameBase))]
public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider
{ {
public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv", ".mpg", ".wmv", ".m4v" };
#if DEBUG #if DEBUG
public const string GAME_NAME = "osu! (development)"; public const string GAME_NAME = "osu! (development)";
#else #else

View File

@ -37,11 +37,13 @@ namespace osu.Game.Overlays.Chat.ChannelList
private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>(); private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>();
public ChannelGroup AnnounceChannelGroup { get; private set; } = null!;
public ChannelGroup PublicChannelGroup { get; private set; } = null!;
public ChannelGroup PrivateChannelGroup { get; private set; } = null!;
private OsuScrollContainer scroll = null!; private OsuScrollContainer scroll = null!;
private SearchContainer groupFlow = null!; private SearchContainer groupFlow = null!;
private ChannelGroup announceChannelGroup = null!;
private ChannelGroup publicChannelGroup = null!;
private ChannelGroup privateChannelGroup = null!;
private ChannelListItem selector = null!; private ChannelListItem selector = null!;
private TextBox searchTextBox = null!; private TextBox searchTextBox = null!;
@ -77,10 +79,10 @@ namespace osu.Game.Overlays.Chat.ChannelList
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
} }
}, },
announceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper()), AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), false),
publicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper()), PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), false),
selector = new ChannelListItem(ChannelListingChannel), selector = new ChannelListItem(ChannelListingChannel),
privateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper()), PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), true),
}, },
}, },
}, },
@ -111,69 +113,70 @@ namespace osu.Game.Overlays.Chat.ChannelList
item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan); item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan);
item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan); item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan);
FillFlowContainer<ChannelListItem> flow = getFlowForChannel(channel); ChannelGroup group = getGroupFromChannel(channel);
channelMap.Add(channel, item); channelMap.Add(channel, item);
flow.Add(item); group.AddChannel(item);
updateVisibility(); updateVisibility();
} }
public void RemoveChannel(Channel channel) public void RemoveChannel(Channel channel)
{ {
if (!channelMap.ContainsKey(channel)) if (!channelMap.TryGetValue(channel, out var item))
return; return;
ChannelListItem item = channelMap[channel]; ChannelGroup group = getGroupFromChannel(channel);
FillFlowContainer<ChannelListItem> flow = getFlowForChannel(channel);
channelMap.Remove(channel); channelMap.Remove(channel);
flow.Remove(item, true); group.RemoveChannel(item);
updateVisibility(); updateVisibility();
} }
public ChannelListItem GetItem(Channel channel) public ChannelListItem GetItem(Channel channel)
{ {
if (!channelMap.ContainsKey(channel)) if (!channelMap.TryGetValue(channel, out var item))
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
return channelMap[channel]; return item;
} }
public void ScrollChannelIntoView(Channel channel) => scroll.ScrollIntoView(GetItem(channel)); public void ScrollChannelIntoView(Channel channel) => scroll.ScrollIntoView(GetItem(channel));
private FillFlowContainer<ChannelListItem> getFlowForChannel(Channel channel) private ChannelGroup getGroupFromChannel(Channel channel)
{ {
switch (channel.Type) switch (channel.Type)
{ {
case ChannelType.Public: case ChannelType.Public:
return publicChannelGroup.ItemFlow; return PublicChannelGroup;
case ChannelType.PM: case ChannelType.PM:
return privateChannelGroup.ItemFlow; return PrivateChannelGroup;
case ChannelType.Announce: case ChannelType.Announce:
return announceChannelGroup.ItemFlow; return AnnounceChannelGroup;
default: default:
return publicChannelGroup.ItemFlow; return PublicChannelGroup;
} }
} }
private void updateVisibility() private void updateVisibility()
{ {
if (announceChannelGroup.ItemFlow.Children.Count == 0) if (AnnounceChannelGroup.ItemFlow.Children.Count == 0)
announceChannelGroup.Hide(); AnnounceChannelGroup.Hide();
else else
announceChannelGroup.Show(); AnnounceChannelGroup.Show();
} }
private partial class ChannelGroup : FillFlowContainer public partial class ChannelGroup : FillFlowContainer
{ {
public readonly FillFlowContainer<ChannelListItem> ItemFlow; private readonly bool sortByRecent;
public readonly ChannelListItemFlow ItemFlow;
public ChannelGroup(LocalisableString label) public ChannelGroup(LocalisableString label, bool sortByRecent)
{ {
this.sortByRecent = sortByRecent;
Direction = FillDirection.Vertical; Direction = FillDirection.Vertical;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
@ -187,7 +190,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
Margin = new MarginPadding { Left = 18, Bottom = 5 }, Margin = new MarginPadding { Left = 18, Bottom = 5 },
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
}, },
ItemFlow = new FillFlowContainer<ChannelListItem> ItemFlow = new ChannelListItemFlow(sortByRecent)
{ {
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -195,6 +198,60 @@ namespace osu.Game.Overlays.Chat.ChannelList
}, },
}; };
} }
public partial class ChannelListItemFlow : FillFlowContainer<ChannelListItem>
{
private readonly bool sortByRecent;
public ChannelListItemFlow(bool sortByRecent)
{
this.sortByRecent = sortByRecent;
}
public void Reflow() => InvalidateLayout();
public override IEnumerable<Drawable> FlowingChildren => sortByRecent
? base.FlowingChildren.OfType<ChannelListItem>().OrderByDescending(i => i.Channel.LastMessageId ?? long.MinValue)
: base.FlowingChildren.OfType<ChannelListItem>().OrderBy(i => i.Channel.Name);
}
public void AddChannel(ChannelListItem item)
{
ItemFlow.Add(item);
if (sortByRecent)
{
item.Channel.NewMessagesArrived += newMessagesArrived;
item.Channel.PendingMessageResolved += pendingMessageResolved;
}
ItemFlow.Reflow();
}
public void RemoveChannel(ChannelListItem item)
{
if (sortByRecent)
{
item.Channel.NewMessagesArrived -= newMessagesArrived;
item.Channel.PendingMessageResolved -= pendingMessageResolved;
}
ItemFlow.Remove(item, true);
}
private void pendingMessageResolved(LocalEchoMessage _, Message __) => ItemFlow.Reflow();
private void newMessagesArrived(IEnumerable<Message> _) => ItemFlow.Reflow();
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
foreach (var item in ItemFlow)
{
item.Channel.NewMessagesArrived -= newMessagesArrived;
item.Channel.PendingMessageResolved -= pendingMessageResolved;
}
}
} }
private partial class ChannelSearchTextBox : BasicSearchTextBox private partial class ChannelSearchTextBox : BasicSearchTextBox

View File

@ -0,0 +1,58 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Overlays
{
public partial class DevBuildBanner : VisibilityContainer
{
[BackgroundDependencyLoader]
private void load(OsuColour colours, TextureStore textures, OsuGameBase game)
{
AutoSizeAxes = Axes.Both;
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
Alpha = 0;
AddRange(new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Font = OsuFont.Numeric.With(weight: FontWeight.Bold, size: 12),
Colour = colours.YellowDark,
Text = @"DEVELOPER BUILD",
},
new Sprite
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Texture = textures.Get(@"Menu/dev-build-footer"),
Scale = new Vector2(0.4f, 1),
Y = 2,
},
});
}
protected override void PopIn()
{
this.FadeIn(1400, Easing.OutQuint);
}
protected override void PopOut()
{
this.FadeOut(500, Easing.OutQuint);
}
}
}

View File

@ -138,34 +138,31 @@ namespace osu.Game.Overlays.Profile.Header.Components
topFifty.ValueColour = colourProvider.Content2; topFifty.ValueColour = colourProvider.Content2;
} }
// reference: https://github.com/ppy/osu-web/blob/adf1e94754ba9625b85eba795f4a310caf169eec/resources/js/profile-page/daily-challenge.tsx#L13-L47 // reference: https://github.com/ppy/osu-web/blob/a97f156014e00ea1aa315140da60542e798a9f06/resources/js/profile-page/daily-challenge.tsx#L13-L47
// Rounding up is needed here to ensure the overlay shows the same colour as osu-web for the play count. public static RankingTier TierForPlayCount(int playCount) => TierForDaily((int)Math.Floor(playCount / 3.0d));
// This is because, for example, 31 / 3 > 10 in JavaScript because floats are used, while here it would
// get truncated to 10 with an integer division and show a lower tier.
public static RankingTier TierForPlayCount(int playCount) => TierForDaily((int)Math.Ceiling(playCount / 3.0d));
public static RankingTier TierForDaily(int daily) public static RankingTier TierForDaily(int daily)
{ {
if (daily > 360) if (daily >= 360)
return RankingTier.Lustrous; return RankingTier.Lustrous;
if (daily > 240) if (daily >= 240)
return RankingTier.Radiant; return RankingTier.Radiant;
if (daily > 120) if (daily >= 120)
return RankingTier.Rhodium; return RankingTier.Rhodium;
if (daily > 60) if (daily >= 60)
return RankingTier.Platinum; return RankingTier.Platinum;
if (daily > 30) if (daily >= 30)
return RankingTier.Gold; return RankingTier.Gold;
if (daily > 10) if (daily >= 10)
return RankingTier.Silver; return RankingTier.Silver;
if (daily > 5) if (daily >= 5)
return RankingTier.Bronze; return RankingTier.Bronze;
return RankingTier.Iron; return RankingTier.Iron;

View File

@ -31,6 +31,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
LabelText = GraphicsSettingsStrings.HitLighting, LabelText = GraphicsSettingsStrings.HitLighting,
Current = config.GetBindable<bool>(OsuSetting.HitLighting) Current = config.GetBindable<bool>(OsuSetting.HitLighting)
}, },
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.StarFountains,
Current = config.GetBindable<bool>(OsuSetting.StarFountains)
},
}; };
} }
} }

View File

@ -35,6 +35,7 @@ using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Utils;
namespace osu.Game.Overlays.SkinEditor namespace osu.Game.Overlays.SkinEditor
{ {
@ -709,7 +710,7 @@ namespace osu.Game.Overlays.SkinEditor
Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException(); Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException();
public IEnumerable<string> HandledExtensions => new[] { ".jpg", ".jpeg", ".png" }; public IEnumerable<string> HandledExtensions => SupportedExtensions.IMAGE_EXTENSIONS;
#endregion #endregion

View File

@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Toolbar
{ {
public partial class TransientUserStatisticsUpdateDisplay : CompositeDrawable public partial class TransientUserStatisticsUpdateDisplay : CompositeDrawable
{ {
public Bindable<UserStatisticsUpdate?> LatestUpdate { get; } = new Bindable<UserStatisticsUpdate?>(); public Bindable<ScoreBasedUserStatisticsUpdate?> LatestUpdate { get; } = new Bindable<ScoreBasedUserStatisticsUpdate?>();
private Statistic<int> globalRank = null!; private Statistic<int> globalRank = null!;
private Statistic<int> pp = null!; private Statistic<int> pp = null!;
@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Toolbar
}; };
if (userStatisticsWatcher != null) if (userStatisticsWatcher != null)
((IBindable<UserStatisticsUpdate?>)LatestUpdate).BindTo(userStatisticsWatcher.LatestUpdate); ((IBindable<ScoreBasedUserStatisticsUpdate?>)LatestUpdate).BindTo(userStatisticsWatcher.LatestUpdate);
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -1,95 +0,0 @@
// 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.Allocation;
using osu.Framework.Development;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays
{
public partial class VersionManager : VisibilityContainer
{
[BackgroundDependencyLoader]
private void load(OsuColour colours, TextureStore textures, OsuGameBase game)
{
AutoSizeAxes = Axes.Both;
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
Alpha = 0;
FillFlowContainer mainFill;
Children = new Drawable[]
{
mainFill = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Children = new Drawable[]
{
new OsuSpriteText
{
Font = OsuFont.GetFont(weight: FontWeight.Bold),
Text = game.Name
},
new OsuSpriteText
{
Colour = DebugUtils.IsDebugBuild ? colours.Red : Color4.White,
Text = game.Version
},
}
},
}
}
};
if (!game.IsDeployedBuild)
{
mainFill.AddRange(new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.Numeric.With(size: 12),
Colour = colours.Yellow,
Text = @"Development Build"
},
new Sprite
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Texture = textures.Get(@"Menu/dev-build-footer"),
},
});
}
}
protected override void PopIn()
{
this.FadeIn(1400, Easing.OutQuint);
}
protected override void PopOut()
{
this.FadeOut(500, Easing.OutQuint);
}
}
}

View File

@ -3,13 +3,12 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Edit.Checks.Components namespace osu.Game.Rulesets.Edit.Checks.Components
{ {
public static class AudioCheckUtils public static class AudioCheckUtils
{ {
public static readonly string[] AUDIO_EXTENSIONS = { "mp3", "ogg", "wav" }; public static bool HasAudioExtension(string filename) => SupportedExtensions.AUDIO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant());
public static bool HasAudioExtension(string filename) => AUDIO_EXTENSIONS.Any(Path.GetExtension(filename).ToLowerInvariant().EndsWith);
} }
} }

View File

@ -205,7 +205,7 @@ namespace osu.Game.Rulesets.Mods
{ {
foreach (var hitObject in hitObjects) foreach (var hitObject in hitObjects)
{ {
if (!(hitObject.HitWindows is HitWindows.EmptyHitWindows)) if (hitObject.HitWindows != HitWindows.Empty)
yield return hitObject; yield return hitObject;
foreach (HitObject nested in getAllApplicableHitObjects(hitObject.NestedHitObjects)) foreach (HitObject nested in getAllApplicableHitObjects(hitObject.NestedHitObjects))

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Scoring namespace osu.Game.Rulesets.Scoring
{ {
@ -20,32 +21,36 @@ namespace osu.Game.Rulesets.Scoring
/// A non-null <see langword="double"/> value if unstable rate could be calculated, /// A non-null <see langword="double"/> value if unstable rate could be calculated,
/// and <see langword="null"/> if unstable rate cannot be calculated due to <paramref name="hitEvents"/> being empty. /// and <see langword="null"/> if unstable rate cannot be calculated due to <paramref name="hitEvents"/> being empty.
/// </returns> /// </returns>
public static double? CalculateUnstableRate(this IEnumerable<HitEvent> hitEvents) public static UnstableRateCalculationResult? CalculateUnstableRate(this IReadOnlyList<HitEvent> hitEvents, UnstableRateCalculationResult? result = null)
{ {
Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null)); Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null));
int count = 0; result ??= new UnstableRateCalculationResult();
double mean = 0;
double sumOfSquares = 0;
foreach (var e in hitEvents) // Handle rewinding in the simplest way possible.
if (hitEvents.Count < result.EventCount + 1)
result = new UnstableRateCalculationResult();
for (int i = result.EventCount; i < hitEvents.Count; i++)
{ {
HitEvent e = hitEvents[i];
if (!AffectsUnstableRate(e)) if (!AffectsUnstableRate(e))
continue; continue;
count++; result.EventCount++;
// Division by gameplay rate is to account for TimeOffset scaling with gameplay rate. // Division by gameplay rate is to account for TimeOffset scaling with gameplay rate.
double currentValue = e.TimeOffset / e.GameplayRate!.Value; double currentValue = e.TimeOffset / e.GameplayRate!.Value;
double nextMean = mean + (currentValue - mean) / count; double nextMean = result.Mean + (currentValue - result.Mean) / result.EventCount;
sumOfSquares += (currentValue - mean) * (currentValue - nextMean); result.SumOfSquares += (currentValue - result.Mean) * (currentValue - nextMean);
mean = nextMean; result.Mean = nextMean;
} }
if (count == 0) if (result.EventCount == 0)
return null; return null;
return 10.0 * Math.Sqrt(sumOfSquares / count); return result;
} }
/// <summary> /// <summary>
@ -65,6 +70,39 @@ namespace osu.Game.Rulesets.Scoring
return timeOffsets.Average(); return timeOffsets.Average();
} }
public static bool AffectsUnstableRate(HitEvent e) => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit(); public static bool AffectsUnstableRate(HitEvent e) => AffectsUnstableRate(e.HitObject, e.Result);
public static bool AffectsUnstableRate(HitObject hitObject, HitResult result) => hitObject.HitWindows != HitWindows.Empty && result.IsHit();
/// <summary>
/// Data type returned by <see cref="HitEventExtensions.CalculateUnstableRate"/> which allows efficient incremental processing.
/// </summary>
/// <remarks>
/// This should be passed back into future <see cref="HitEventExtensions.CalculateUnstableRate"/> calls as a parameter.
///
/// The optimisations used here rely on hit events being a consecutive sequence from a single gameplay session.
/// When a new gameplay session is started, any existing results should be disposed.
/// </remarks>
public class UnstableRateCalculationResult
{
/// <summary>
/// Total events processed. For internal incremental calculation use.
/// </summary>
public int EventCount;
/// <summary>
/// Last sum-of-squares value. For internal incremental calculation use.
/// </summary>
public double SumOfSquares;
/// <summary>
/// Last mean value. For internal incremental calculation use.
/// </summary>
public double Mean;
/// <summary>
/// The unstable rate.
/// </summary>
public double Result => EventCount == 0 ? 0 : 10.0 * Math.Sqrt(SumOfSquares / EventCount);
}
} }
} }

View File

@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Scoring
/// An empty <see cref="HitWindows"/> with only <see cref="HitResult.Miss"/> and <see cref="HitResult.Perfect"/>. /// An empty <see cref="HitWindows"/> with only <see cref="HitResult.Miss"/> and <see cref="HitResult.Perfect"/>.
/// No time values are provided (meaning instantaneous hit or miss). /// No time values are provided (meaning instantaneous hit or miss).
/// </summary> /// </summary>
public static HitWindows Empty => new EmptyHitWindows(); public static HitWindows Empty { get; } = new EmptyHitWindows();
public HitWindows() public HitWindows()
{ {
@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Scoring
/// </summary> /// </summary>
protected virtual DifficultyRange[] GetRanges() => base_ranges; protected virtual DifficultyRange[] GetRanges() => base_ranges;
public class EmptyHitWindows : HitWindows private class EmptyHitWindows : HitWindows
{ {
private static readonly DifficultyRange[] ranges = private static readonly DifficultyRange[] ranges =
{ {

View File

@ -78,7 +78,7 @@ namespace osu.Game.Scoring
/// Perform a lookup query on available <see cref="ScoreInfo"/>s. /// Perform a lookup query on available <see cref="ScoreInfo"/>s.
/// </summary> /// </summary>
/// <param name="query">The query.</param> /// <param name="query">The query.</param>
/// <returns>The first result for the provided query, or null if no results were found.</returns> /// <returns>The first result for the provided query in its detached form, or null if no results were found.</returns>
public ScoreInfo? Query(Expression<Func<ScoreInfo, bool>> query) public ScoreInfo? Query(Expression<Func<ScoreInfo, bool>> query)
{ {
return Realm.Run(r => r.All<ScoreInfo>().FirstOrDefault(query)?.Detach()); return Realm.Run(r => r.All<ScoreInfo>().FirstOrDefault(query)?.Detach());
@ -88,8 +88,14 @@ namespace osu.Game.Scoring
{ {
ScoreInfo? databasedScoreInfo = null; ScoreInfo? databasedScoreInfo = null;
if (originalScoreInfo is ScoreInfo scoreInfo && !string.IsNullOrEmpty(scoreInfo.Hash)) if (originalScoreInfo is ScoreInfo scoreInfo)
databasedScoreInfo = Query(s => s.Hash == scoreInfo.Hash); {
if (scoreInfo.IsManaged)
return scoreInfo.Detach();
if (!string.IsNullOrEmpty(scoreInfo.Hash))
databasedScoreInfo = Query(s => s.Hash == scoreInfo.Hash);
}
if (originalScoreInfo.OnlineID > 0) if (originalScoreInfo.OnlineID > 0)
databasedScoreInfo ??= Query(s => s.OnlineID == originalScoreInfo.OnlineID); databasedScoreInfo ??= Query(s => s.OnlineID == originalScoreInfo.OnlineID);

View File

@ -258,6 +258,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void resetTernaryStates() private void resetTernaryStates()
{ {
if (SelectedItems.Count > 0)
return;
SelectionNewComboState.Value = TernaryState.False; SelectionNewComboState.Value = TernaryState.False;
AutoSelectionBankEnabled.Value = true; AutoSelectionBankEnabled.Value = true;
SelectionAdditionBanksEnabled.Value = true; SelectionAdditionBanksEnabled.Value = true;

View File

@ -10,6 +10,7 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Utils;
namespace osu.Game.Screens.Edit.Setup namespace osu.Game.Screens.Edit.Setup
{ {
@ -48,12 +49,12 @@ namespace osu.Game.Screens.Edit.Setup
Children = new Drawable[] Children = new Drawable[]
{ {
backgroundChooser = new FormFileSelector(".jpg", ".jpeg", ".png") backgroundChooser = new FormFileSelector(SupportedExtensions.IMAGE_EXTENSIONS)
{ {
Caption = GameplaySettingsStrings.BackgroundHeader, Caption = GameplaySettingsStrings.BackgroundHeader,
PlaceholderText = EditorSetupStrings.ClickToSelectBackground, PlaceholderText = EditorSetupStrings.ClickToSelectBackground,
}, },
audioTrackChooser = new FormFileSelector(".mp3", ".ogg") audioTrackChooser = new FormFileSelector(SupportedExtensions.AUDIO_EXTENSIONS)
{ {
Caption = EditorSetupStrings.AudioTrack, Caption = EditorSetupStrings.AudioTrack,
PlaceholderText = EditorSetupStrings.ClickToSelectTrack, PlaceholderText = EditorSetupStrings.ClickToSelectTrack,

View File

@ -79,9 +79,6 @@ namespace osu.Game.Screens.Menu
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private IDialogOverlay dialogOverlay { get; set; } private IDialogOverlay dialogOverlay { get; set; }
[Resolved(canBeNull: true)]
private VersionManager versionManager { get; set; }
protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault(); protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault();
protected override bool PlayExitSound => false; protected override bool PlayExitSound => false;
@ -294,16 +291,6 @@ namespace osu.Game.Screens.Menu
} }
} }
protected override void Update()
{
base.Update();
bottomElementsFlow.Margin = new MarginPadding
{
Bottom = (versionManager?.DrawHeight + 5) ?? 0
};
}
protected override void LogoSuspending(OsuLogo logo) protected override void LogoSuspending(OsuLogo logo)
{ {
var seq = logo.FadeOut(300, Easing.InSine) var seq = logo.FadeOut(300, Easing.InSine)

View File

@ -7,12 +7,14 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Game.Localisation;
namespace osu.Game.Screens.Menu namespace osu.Game.Screens.Menu
{ {
@ -78,49 +80,49 @@ namespace osu.Game.Screens.Menu
static void formatRegular(SpriteText t) => t.Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular); static void formatRegular(SpriteText t) => t.Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular);
static void formatSemiBold(SpriteText t) => t.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold); static void formatSemiBold(SpriteText t) => t.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold);
string tip = getRandomTip(); var tip = getRandomTip();
textFlow.Clear(); textFlow.Clear();
textFlow.AddParagraph("a tip for you:", formatSemiBold); textFlow.AddParagraph(MenuTipStrings.MenuTipTitle, formatSemiBold);
textFlow.AddParagraph(tip, formatRegular); textFlow.AddParagraph(tip, formatRegular);
this.FadeInFromZero(200, Easing.OutQuint) this.FadeInFromZero(200, Easing.OutQuint)
.Delay(1000 + 80 * tip.Length) .Delay(1000 + 80 * tip.ToString().Length)
.Then() .Then()
.FadeOutFromOne(2000, Easing.OutQuint); .FadeOutFromOne(2000, Easing.OutQuint);
} }
private string getRandomTip() private LocalisableString getRandomTip()
{ {
string[] tips = LocalisableString[] tips =
{ {
"Press Ctrl-T anywhere in the game to toggle the toolbar!", MenuTipStrings.ToggleToolbarShortcut,
"Press Ctrl-O anywhere in the game to access options!", MenuTipStrings.GameSettingsShortcut,
"All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!", MenuTipStrings.DynamicSettings,
"New features are coming online every update. Make sure to stay up-to-date!", MenuTipStrings.NewFeaturesAreComingOnline,
"If you find the UI too large or small, try adjusting UI scale in settings!", MenuTipStrings.UIScalingSettings,
"Try adjusting the \"Screen Scaling\" mode to change your gameplay or UI area, even in fullscreen!", MenuTipStrings.ScreenScalingSettings,
"What used to be \"osu!direct\" is available to all users just like on the website. You can access it anywhere using Ctrl-B!", MenuTipStrings.FreeOsuDirect,
"Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!", MenuTipStrings.ReplaySeeking,
"Multithreading support means that even with low \"FPS\" your input and judgements will be accurate!", MenuTipStrings.MultithreadingSupport,
"Try scrolling right in mod select to find a bunch of new fun mods!", MenuTipStrings.TryNewMods,
"Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!", MenuTipStrings.EmbeddedWebContent,
"Get more details, hide or delete a beatmap by right-clicking on its panel at song select!", MenuTipStrings.BeatmapRightClick,
"All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!", MenuTipStrings.TemporaryDeleteOperations,
"Check out the \"playlists\" system, which lets users create their own custom and permanent leaderboards!", MenuTipStrings.DiscoverPlaylists,
"Toggle advanced frame / thread statistics with Ctrl-F11!", MenuTipStrings.ToggleAdvancedFPSCounter,
"Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!", MenuTipStrings.GlobalStatisticsShortcut,
"You can pause during a replay by pressing Space!", MenuTipStrings.ReplayPausing,
"Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!", MenuTipStrings.ConfigurableHotkeys,
"When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!", MenuTipStrings.PeekHUDWhenHidden,
"Your gameplay HUD can be customized by using the skin layout editor. Open it at any time via Ctrl-Shift-S!", MenuTipStrings.SkinEditor,
"Drag and drop any image into the skin editor to load it in quickly!", MenuTipStrings.DragAndDropImageInSkinEditor,
"You can create mod presets to make toggling your favorite mod combinations easier!", MenuTipStrings.ModPresets,
"Many mods have customisation settings that drastically change how they function. Click the Mod Customisation button in mod select to view settings!", MenuTipStrings.ModCustomisationSettings,
"Press Ctrl-Shift-R to switch to a random skin!", MenuTipStrings.RandomSkinShortcut,
"While watching a replay, press Ctrl-H to toggle replay settings!", MenuTipStrings.ToggleReplaySettingsShortcut,
"You can easily copy the mods from scores on a leaderboard by right-clicking on them!", MenuTipStrings.CopyModsFromScore,
"Ctrl-Enter at song select will start a beatmap in autoplay mode!" MenuTipStrings.AutoplayBeatmapShortcut
}; };
return tips[RNG.Next(0, tips.Length)]; return tips[RNG.Next(0, tips.Length)];

View File

@ -345,7 +345,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
private void presentScore(long id) private void presentScore(long id)
{ {
if (this.IsCurrentScreen()) if (this.IsCurrentScreen())
this.Push(new PlaylistItemScoreResultsScreen(room.RoomID!.Value, playlistItem, id)); this.Push(new PlaylistItemScoreResultsScreen(id, room.RoomID!.Value, playlistItem));
} }
private void onRoomScoreSet(MultiplayerRoomScoreSetEvent e) private void onRoomScoreSet(MultiplayerRoomScoreSetEvent e)

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -22,9 +23,14 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Playlists;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using Container = osu.Framework.Graphics.Containers.Container; using Container = osu.Framework.Graphics.Containers.Container;
@ -48,6 +54,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private LoungeSubScreen? lounge { get; set; } private LoungeSubScreen? lounge { get; set; }
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly BindableWithCurrent<Room?> selectedRoom = new BindableWithCurrent<Room?>(); private readonly BindableWithCurrent<Room?> selectedRoom = new BindableWithCurrent<Room?>();
private Sample? sampleSelect; private Sample? sampleSelect;
private Sample? sampleJoin; private Sample? sampleJoin;
@ -144,13 +156,34 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
public Popover GetPopover() => new PasswordEntryPopover(Room); public Popover GetPopover() => new PasswordEntryPopover(Room);
public MenuItem[] ContextMenuItems => new MenuItem[] public MenuItem[] ContextMenuItems
{ {
new OsuMenuItem("Create copy", MenuItemType.Standard, () => get
{ {
lounge?.OpenCopy(Room); var items = new List<MenuItem>
}) {
}; new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
{
lounge?.OpenCopy(Room);
})
};
if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && Room.Status is not RoomStatusEnded)
{
items.Add(new OsuMenuItem("Close playlist", MenuItemType.Destructive, () =>
{
dialogOverlay?.Push(new ClosePlaylistDialog(Room, () =>
{
var request = new ClosePlaylistRequest(Room.RoomID!.Value);
request.Success += () => lounge?.RefreshRooms();
api.Queue(request);
}));
}));
}
return items.ToArray();
}
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {

View File

@ -379,6 +379,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
this.Push(CreateRoomSubScreen(room)); this.Push(CreateRoomSubScreen(room));
} }
public void RefreshRooms() => ListingPollingComponent.PollImmediately();
private void updateLoadingLayer() private void updateLoadingLayer()
{ {
if (operationInProgress.Value || !ListingPollingComponent.InitialRoomsReceived.Value) if (operationInProgress.Value || !ListingPollingComponent.InitialRoomsReceived.Value)

View File

@ -71,7 +71,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
protected RulesetStore Rulesets { get; private set; } = null!; protected RulesetStore Rulesets { get; private set; } = null!;
[Resolved] [Resolved]
private IAPIProvider api { get; set; } = null!; protected IAPIProvider API { get; private set; } = null!;
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
protected OnlinePlayScreen? ParentScreen { get; private set; } protected OnlinePlayScreen? ParentScreen { get; private set; }
@ -80,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
private PreviewTrackManager previewTrackManager { get; set; } = null!; private PreviewTrackManager previewTrackManager { get; set; } = null!;
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private IDialogOverlay? dialogOverlay { get; set; } protected IDialogOverlay? DialogOverlay { get; private set; }
[Cached] [Cached]
private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
@ -282,7 +282,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
} }
} }
protected virtual bool IsConnected => api.State.Value == APIState.Online; protected virtual bool IsConnected => API.State.Value == APIState.Online;
public override bool OnBackButton() public override bool OnBackButton()
{ {
@ -361,17 +361,17 @@ namespace osu.Game.Screens.OnlinePlay.Match
bool hasUnsavedChanges = Room.RoomID == null && Room.Playlist.Count > 0; bool hasUnsavedChanges = Room.RoomID == null && Room.Playlist.Count > 0;
if (dialogOverlay == null || !hasUnsavedChanges) if (DialogOverlay == null || !hasUnsavedChanges)
return true; return true;
// if the dialog is already displayed, block exiting until the user explicitly makes a decision. // if the dialog is already displayed, block exiting until the user explicitly makes a decision.
if (dialogOverlay.CurrentDialog is ConfirmDiscardChangesDialog discardChangesDialog) if (DialogOverlay.CurrentDialog is ConfirmDiscardChangesDialog discardChangesDialog)
{ {
discardChangesDialog.Flash(); discardChangesDialog.Flash();
return false; return false;
} }
dialogOverlay.Push(new ConfirmDiscardChangesDialog(() => DialogOverlay.Push(new ConfirmDiscardChangesDialog(() =>
{ {
ExitConfirmed = true; ExitConfirmed = true;
settingsOverlay.Hide(); settingsOverlay.Hide();

View File

@ -7,7 +7,7 @@ using osu.Game.Screens.OnlinePlay.Playlists;
namespace osu.Game.Screens.OnlinePlay.Multiplayer namespace osu.Game.Screens.OnlinePlay.Multiplayer
{ {
public partial class MultiplayerResultsScreen : PlaylistItemUserResultsScreen public partial class MultiplayerResultsScreen : PlaylistItemScoreResultsScreen
{ {
public MultiplayerResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem) public MultiplayerResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem)
: base(score, roomId, playlistItem) : base(score, roomId, playlistItem)

View File

@ -0,0 +1,19 @@
// 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.Game.Online.Rooms;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
public partial class ClosePlaylistDialog : DeletionDialog
{
public ClosePlaylistDialog(Room room, Action closeAction)
{
HeaderText = "Are you sure you want to close the following playlist:";
BodyText = room.Name;
DangerousAction = closeAction;
}
}
}

View File

@ -191,8 +191,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{ {
var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray();
// Invoke callback to add the scores. // Invoke callback to add the scores. Exclude the score provided to this screen since it's added already.
callback.Invoke(scoreInfos); callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID));
return scoreInfos; return scoreInfos;
} }

View File

@ -11,13 +11,19 @@ using osu.Game.Scoring;
namespace osu.Game.Screens.OnlinePlay.Playlists namespace osu.Game.Screens.OnlinePlay.Playlists
{ {
/// <summary> /// <summary>
/// Shows a selected arbitrary score for a playlist item, with scores around included. /// Shows a given score in a playlist item, with scores around included.
/// </summary> /// </summary>
public partial class PlaylistItemScoreResultsScreen : PlaylistItemResultsScreen public partial class PlaylistItemScoreResultsScreen : PlaylistItemResultsScreen
{ {
private readonly long scoreId; private readonly long scoreId;
public PlaylistItemScoreResultsScreen(long roomId, PlaylistItem playlistItem, long scoreId) public PlaylistItemScoreResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem)
: base(score, roomId, playlistItem)
{
scoreId = score.OnlineID;
}
public PlaylistItemScoreResultsScreen(long scoreId, long roomId, PlaylistItem playlistItem)
: base(null, roomId, playlistItem) : base(null, roomId, playlistItem)
{ {
this.scoreId = scoreId; this.scoreId = scoreId;
@ -28,9 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override ScoreInfo[] PerformSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null) protected override ScoreInfo[] PerformSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null)
{ {
var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot); var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot);
Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId));
Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(score => score.OnlineID == scoreId));
return scoreInfos; return scoreInfos;
} }
} }

View File

@ -0,0 +1,41 @@
// 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 osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Scoring;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
/// <summary>
/// Shows a user's best score in a playlist item, with scores around included.
/// </summary>
public partial class PlaylistItemUserBestResultsScreen : PlaylistItemResultsScreen
{
private readonly int userId;
public PlaylistItemUserBestResultsScreen(long roomId, PlaylistItem playlistItem, int userId)
: base(null, roomId, playlistItem)
{
this.userId = userId;
}
protected override APIRequest<MultiplayerScore> CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId);
protected override ScoreInfo[] PerformSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null)
{
var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot);
Schedule(() =>
{
// Prefer selecting the local user's score, or otherwise default to the first visible score.
SelectedScore.Value ??= scoreInfos.FirstOrDefault(s => s.UserID == userId) ?? scoreInfos.FirstOrDefault();
});
return scoreInfos;
}
}
}

View File

@ -1,46 +0,0 @@
// 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 osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Scoring;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
/// <summary>
/// Shows the user's best score for a given playlist item, with scores around included.
/// </summary>
public partial class PlaylistItemUserResultsScreen : PlaylistItemResultsScreen
{
public PlaylistItemUserResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem)
: base(score, roomId, playlistItem)
{
}
protected override APIRequest<MultiplayerScore> CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, API.LocalUser.Value.Id);
protected override ScoreInfo[] PerformSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null)
{
var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray();
// Select a score if we don't already have one selected.
// Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).
if (SelectedScore.Value == null)
{
Schedule(() =>
{
// Prefer selecting the local user's score, or otherwise default to the first visible score.
SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.OnlineID == API.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault();
});
}
// Invoke callback to add the scores. Exclude the user's current score which was added previously.
callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID));
return scoreInfos;
}
}
}

View File

@ -56,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override ResultsScreen CreateResults(ScoreInfo score) protected override ResultsScreen CreateResults(ScoreInfo score)
{ {
Debug.Assert(Room.RoomID != null); Debug.Assert(Room.RoomID != null);
return new PlaylistItemUserResultsScreen(score, Room.RoomID.Value, PlaylistItem) return new PlaylistItemScoreResultsScreen(score, Room.RoomID.Value, PlaylistItem)
{ {
AllowRetry = true, AllowRetry = true,
ShowUserStatistics = true, ShowUserStatistics = true,

View File

@ -2,9 +2,14 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.ComponentModel;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osuTK; using osuTK;
namespace osu.Game.Screens.OnlinePlay.Playlists namespace osu.Game.Screens.OnlinePlay.Playlists
@ -12,22 +17,104 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
public partial class PlaylistsRoomFooter : CompositeDrawable public partial class PlaylistsRoomFooter : CompositeDrawable
{ {
public Action? OnStart; public Action? OnStart;
public Action? OnClose;
private readonly Room room;
private DangerousRoundedButton closeButton = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
public PlaylistsRoomFooter(Room room) public PlaylistsRoomFooter(Room room)
{
this.room = room;
}
[BackgroundDependencyLoader]
private void load()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
InternalChildren = new[] InternalChild = new FillFlowContainer
{ {
new PlaylistsReadyButton(room) AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10),
Children = new Drawable[]
{ {
Anchor = Anchor.Centre, new PlaylistsReadyButton(room)
Origin = Anchor.Centre, {
RelativeSizeAxes = Axes.Y, Anchor = Anchor.Centre,
Size = new Vector2(600, 1), Origin = Anchor.Centre,
Action = () => OnStart?.Invoke() RelativeSizeAxes = Axes.Y,
Size = new Vector2(600, 1),
Action = () => OnStart?.Invoke()
},
closeButton = new DangerousRoundedButton
{
Text = "Close",
Action = () => OnClose?.Invoke(),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(120, 1),
Alpha = 0,
RelativeSizeAxes = Axes.Y,
}
} }
}; };
} }
protected override void LoadComplete()
{
base.LoadComplete();
room.PropertyChanged += onRoomChanged;
updateState();
}
private void hideCloseButton()
{
closeButton.ResizeWidthTo(0, 100, Easing.OutQuint)
.Then().FadeOut().Expire();
}
private void onRoomChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(Room.Status):
case nameof(Room.Host):
case nameof(Room.StartDate):
updateState();
break;
}
}
private void updateState()
{
TimeSpan? deletionGracePeriodRemaining = room.StartDate?.AddMinutes(5) - DateTimeOffset.Now;
if (room.Host?.Id == api.LocalUser.Value.Id)
{
if (deletionGracePeriodRemaining > TimeSpan.Zero && room.Status is not RoomStatusEnded)
{
closeButton.FadeIn();
using (BeginDelayedSequence(deletionGracePeriodRemaining.Value.TotalMilliseconds))
hideCloseButton();
}
else if (closeButton.Alpha > 0)
hideCloseButton();
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
room.PropertyChanged -= onRoomChanged;
}
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -12,7 +13,10 @@ using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Input; using osu.Game.Input;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Match.Components;
@ -32,6 +36,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private readonly IBindable<bool> isIdle = new BindableBool(); private readonly IBindable<bool> isIdle = new BindableBool();
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IdleTracker? idleTracker { get; set; } private IdleTracker? idleTracker { get; set; }
@ -143,7 +150,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
RequestResults = item => RequestResults = item =>
{ {
Debug.Assert(Room.RoomID != null); Debug.Assert(Room.RoomID != null);
ParentScreen?.Push(new PlaylistItemUserResultsScreen(null, Room.RoomID.Value, item)); ParentScreen?.Push(new PlaylistItemUserBestResultsScreen(Room.RoomID.Value, item, api.LocalUser.Value.Id));
} }
} }
}, },
@ -255,7 +262,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override Drawable CreateFooter() => new PlaylistsRoomFooter(Room) protected override Drawable CreateFooter() => new PlaylistsRoomFooter(Room)
{ {
OnStart = StartPlay OnStart = StartPlay,
OnClose = closePlaylist,
}; };
protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new PlaylistsRoomSettingsOverlay(room) protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new PlaylistsRoomSettingsOverlay(room)
@ -273,6 +281,20 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
Logger.Log($"Polling adjusted (selection: {selectionPollingComponent.TimeBetweenPolls.Value})"); Logger.Log($"Polling adjusted (selection: {selectionPollingComponent.TimeBetweenPolls.Value})");
} }
private void closePlaylist()
{
DialogOverlay?.Push(new ClosePlaylistDialog(Room, () =>
{
var request = new ClosePlaylistRequest(Room.RoomID!.Value);
request.Success += () =>
{
Room.Status = new RoomStatusEnded();
Room.EndDate = DateTimeOffset.UtcNow;
};
API.Queue(request);
}));
}
protected override Screen CreateGameplayScreen(PlaylistItem selectedItem) protected override Screen CreateGameplayScreen(PlaylistItem selectedItem)
{ {
return new PlayerLoader(() => new PlaylistsPlayer(Room, selectedItem) return new PlayerLoader(() => new PlaylistsPlayer(Room, selectedItem)

View File

@ -28,6 +28,8 @@ namespace osu.Game.Screens.Play.HUD
private const float alpha_when_invalid = 0.3f; private const float alpha_when_invalid = 0.3f;
private readonly Bindable<bool> valid = new Bindable<bool>(); private readonly Bindable<bool> valid = new Bindable<bool>();
private HitEventExtensions.UnstableRateCalculationResult? unstableRateResult;
[Resolved] [Resolved]
private ScoreProcessor scoreProcessor { get; set; } = null!; private ScoreProcessor scoreProcessor { get; set; } = null!;
@ -44,9 +46,6 @@ namespace osu.Game.Screens.Play.HUD
DrawableCount.FadeTo(e.NewValue ? 1 : alpha_when_invalid, 1000, Easing.OutQuint)); DrawableCount.FadeTo(e.NewValue ? 1 : alpha_when_invalid, 1000, Easing.OutQuint));
} }
private bool changesUnstableRate(JudgementResult judgement)
=> !(judgement.HitObject.HitWindows is HitWindows.EmptyHitWindows) && judgement.IsHit;
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -56,13 +55,20 @@ namespace osu.Game.Screens.Play.HUD
updateDisplay(); updateDisplay();
} }
private void updateDisplay(JudgementResult _) => Scheduler.AddOnce(updateDisplay); private void updateDisplay(JudgementResult result)
{
if (HitEventExtensions.AffectsUnstableRate(result.HitObject, result.Type))
Scheduler.AddOnce(updateDisplay);
}
private void updateDisplay() private void updateDisplay()
{ {
double? unstableRate = scoreProcessor.HitEvents.CalculateUnstableRate(); unstableRateResult = scoreProcessor.HitEvents.CalculateUnstableRate(unstableRateResult);
double? unstableRate = unstableRateResult?.Result;
valid.Value = unstableRate != null; valid.Value = unstableRate != null;
if (unstableRate != null) if (unstableRate != null)
Current.Value = (int)Math.Round(unstableRate.Value); Current.Value = (int)Math.Round(unstableRate.Value);
} }

View File

@ -5,8 +5,10 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
@ -18,9 +20,13 @@ namespace osu.Game.Screens.Play
private StarFountain leftFountain = null!; private StarFountain leftFountain = null!;
private StarFountain rightFountain = null!; private StarFountain rightFountain = null!;
private Bindable<bool> kiaiStarFountains = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OsuConfigManager config)
{ {
kiaiStarFountains = config.GetBindable<bool>(OsuSetting.StarFountains);
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Children = new[] Children = new[]
@ -48,6 +54,9 @@ namespace osu.Game.Screens.Play
{ {
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
if (!kiaiStarFountains.Value)
return;
if (effectPoint.KiaiMode && !isTriggered) if (effectPoint.KiaiMode && !isTriggered)
{ {
bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - effectPoint.Time) < 500; bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - effectPoint.Time) < 500;

View File

@ -28,6 +28,7 @@ using osu.Game.Localisation;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Performance; using osu.Game.Performance;
using osu.Game.Scoring;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -78,6 +79,8 @@ namespace osu.Game.Screens.Play
private FillFlowContainer disclaimers = null!; private FillFlowContainer disclaimers = null!;
private OsuScrollContainer settingsScroll = null!; private OsuScrollContainer settingsScroll = null!;
private Bindable<ScoreInfo?> lastScore = null!;
private Bindable<bool> showStoryboards = null!; private Bindable<bool> showStoryboards = null!;
private bool backgroundBrightnessReduction; private bool backgroundBrightnessReduction;
@ -179,6 +182,8 @@ namespace osu.Game.Screens.Play
{ {
muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce); muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce);
batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce); batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce);
lastScore = sessionStatics.GetBindable<ScoreInfo?>(Static.LastLocalUserScore);
showStoryboards = config.GetBindable<bool>(OsuSetting.ShowStoryboard); showStoryboards = config.GetBindable<bool>(OsuSetting.ShowStoryboard);
const float padding = 25; const float padding = 25;
@ -347,6 +352,8 @@ namespace osu.Game.Screens.Play
highPerformanceSession?.Dispose(); highPerformanceSession?.Dispose();
highPerformanceSession = null; highPerformanceSession = null;
lastScore.Value = null;
return base.OnExiting(e); return base.OnExiting(e);
} }

View File

@ -32,7 +32,7 @@ namespace osu.Game.Screens.Play
private readonly Func<Task<ScoreInfo>>? importFailedScore; private readonly Func<Task<ScoreInfo>>? importFailedScore;
private ScoreInfo? importedScore; private Live<ScoreInfo>? importedScore;
private DownloadButton button = null!; private DownloadButton button = null!;
@ -55,7 +55,7 @@ namespace osu.Game.Screens.Play
switch (state.Value) switch (state.Value)
{ {
case DownloadState.LocallyAvailable: case DownloadState.LocallyAvailable:
game?.PresentScore(importedScore, ScorePresentType.Gameplay); game?.PresentScore(importedScore?.Value, ScorePresentType.Gameplay);
break; break;
case DownloadState.NotDownloaded: case DownloadState.NotDownloaded:
@ -65,7 +65,7 @@ namespace osu.Game.Screens.Play
{ {
Task.Run(importFailedScore).ContinueWith(t => Task.Run(importFailedScore).ContinueWith(t =>
{ {
importedScore = realm.Run(r => r.Find<ScoreInfo>(t.GetResultSafely().ID)?.Detach()); importedScore = realm.Run<Live<ScoreInfo>?>(r => r.Find<ScoreInfo>(t.GetResultSafely().ID)?.ToLive(realm));
Schedule(() => state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded); Schedule(() => state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded);
}).FireAndForget(); }).FireAndForget();
} }
@ -77,7 +77,7 @@ namespace osu.Game.Screens.Play
if (player != null) if (player != null)
{ {
importedScore = realm.Run(r => r.Find<ScoreInfo>(player.Score.ScoreInfo.ID)?.Detach()); importedScore = realm.Run(r => r.Find<ScoreInfo>(player.Score.ScoreInfo.ID)?.ToLive(realm));
state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded; state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded;
} }
@ -137,7 +137,7 @@ namespace osu.Game.Screens.Play
{ {
if (state.NewValue != DownloadState.LocallyAvailable) return; if (state.NewValue != DownloadState.LocallyAvailable) return;
if (importedScore != null) scoreManager.Export(importedScore); if (importedScore != null) scoreManager.Export(importedScore.Value);
this.state.ValueChanged -= exportWhenReady; this.state.ValueChanged -= exportWhenReady;
} }

View File

@ -58,7 +58,7 @@ namespace osu.Game.Screens.Ranking.Statistics
/// <param name="hitEvents">The <see cref="HitEvent"/>s to display the timing distribution of.</param> /// <param name="hitEvents">The <see cref="HitEvent"/>s to display the timing distribution of.</param>
public HitEventTimingDistributionGraph(IReadOnlyList<HitEvent> hitEvents) public HitEventTimingDistributionGraph(IReadOnlyList<HitEvent> hitEvents)
{ {
this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsBasic() && e.Result.IsHit()).ToList(); this.hitEvents = hitEvents.Where(e => e.HitObject.HitWindows != HitWindows.Empty && e.Result.IsBasic() && e.Result.IsHit()).ToList();
bins = Enumerable.Range(0, total_timing_distribution_bins).Select(_ => new Dictionary<HitResult, int>()).ToArray<IDictionary<HitResult, int>>(); bins = Enumerable.Range(0, total_timing_distribution_bins).Select(_ => new Dictionary<HitResult, int>()).ToArray<IDictionary<HitResult, int>>();
} }

View File

@ -15,10 +15,10 @@ namespace osu.Game.Screens.Ranking.Statistics
/// Creates and computes an <see cref="UnstableRate"/> statistic. /// Creates and computes an <see cref="UnstableRate"/> statistic.
/// </summary> /// </summary>
/// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the unstable rate based on.</param> /// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the unstable rate based on.</param>
public UnstableRate(IEnumerable<HitEvent> hitEvents) public UnstableRate(IReadOnlyList<HitEvent> hitEvents)
: base("Unstable Rate") : base("Unstable Rate")
{ {
Value = hitEvents.CalculateUnstableRate(); Value = hitEvents.CalculateUnstableRate()?.Result;
} }
protected override string DisplayValue(double? value) => value == null ? "(not available)" : value.Value.ToString(@"N2"); protected override string DisplayValue(double? value) => value == null ? "(not available)" : value.Value.ToString(@"N2");

View File

@ -14,7 +14,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User
{ {
private const float transition_duration = 300; private const float transition_duration = 300;
public Bindable<UserStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<UserStatisticsUpdate?>(); public Bindable<ScoreBasedUserStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<ScoreBasedUserStatisticsUpdate?>();
private LoadingLayer loadingLayer = null!; private LoadingLayer loadingLayer = null!;
private GridContainer content = null!; private GridContainer content = null!;
@ -86,7 +86,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User
FinishTransforms(true); FinishTransforms(true);
} }
private void onUpdateReceived(ValueChangedEvent<UserStatisticsUpdate?> update) private void onUpdateReceived(ValueChangedEvent<ScoreBasedUserStatisticsUpdate?> update)
{ {
if (update.NewValue == null) if (update.NewValue == null)
{ {

View File

@ -19,7 +19,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User
{ {
public abstract partial class RankingChangeRow<T> : CompositeDrawable public abstract partial class RankingChangeRow<T> : CompositeDrawable
{ {
public Bindable<UserStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<UserStatisticsUpdate?>(); public Bindable<ScoreBasedUserStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<ScoreBasedUserStatisticsUpdate?>();
private readonly Func<UserStatistics, T> accessor; private readonly Func<UserStatistics, T> accessor;
@ -113,7 +113,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User
StatisticsUpdate.BindValueChanged(onStatisticsUpdate, true); StatisticsUpdate.BindValueChanged(onStatisticsUpdate, true);
} }
private void onStatisticsUpdate(ValueChangedEvent<UserStatisticsUpdate?> statisticsUpdate) private void onStatisticsUpdate(ValueChangedEvent<ScoreBasedUserStatisticsUpdate?> statisticsUpdate)
{ {
var update = statisticsUpdate.NewValue; var update = statisticsUpdate.NewValue;

View File

@ -18,9 +18,9 @@ namespace osu.Game.Screens.Ranking.Statistics
{ {
private readonly ScoreInfo achievedScore; private readonly ScoreInfo achievedScore;
internal readonly Bindable<UserStatisticsUpdate?> DisplayedUserStatisticsUpdate = new Bindable<UserStatisticsUpdate?>(); internal readonly Bindable<ScoreBasedUserStatisticsUpdate?> DisplayedUserStatisticsUpdate = new Bindable<ScoreBasedUserStatisticsUpdate?>();
private IBindable<UserStatisticsUpdate?> latestGlobalStatisticsUpdate = null!; private IBindable<ScoreBasedUserStatisticsUpdate?> latestGlobalStatisticsUpdate = null!;
public UserStatisticsPanel(ScoreInfo achievedScore) public UserStatisticsPanel(ScoreInfo achievedScore)
{ {

View File

@ -29,7 +29,6 @@ using osu.Game.Input.Bindings;
using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Carousel;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
using Realms;
namespace osu.Game.Screens.Select namespace osu.Game.Screens.Select
{ {
@ -207,8 +206,6 @@ namespace osu.Game.Screens.Select
private CarouselRoot root; private CarouselRoot root;
private IDisposable? subscriptionBeatmaps;
private readonly DrawablePool<DrawableCarouselBeatmapSet> setPool = new DrawablePool<DrawableCarouselBeatmapSet>(100); private readonly DrawablePool<DrawableCarouselBeatmapSet> setPool = new DrawablePool<DrawableCarouselBeatmapSet>(100);
private Sample? spinSample; private Sample? spinSample;
@ -258,13 +255,6 @@ namespace osu.Game.Screens.Select
} }
} }
protected override void LoadComplete()
{
base.LoadComplete();
subscriptionBeatmaps = realm.RegisterForNotifications(r => r.All<BeatmapInfo>().Where(b => !b.Hidden), beatmapsChanged);
}
private readonly HashSet<BeatmapSetInfo> setsRequiringUpdate = new HashSet<BeatmapSetInfo>(); private readonly HashSet<BeatmapSetInfo> setsRequiringUpdate = new HashSet<BeatmapSetInfo>();
private readonly HashSet<BeatmapSetInfo> setsRequiringRemoval = new HashSet<BeatmapSetInfo>(); private readonly HashSet<BeatmapSetInfo> setsRequiringRemoval = new HashSet<BeatmapSetInfo>();
@ -366,35 +356,6 @@ namespace osu.Game.Screens.Select
BeatmapSetInfo? fetchFromID(Guid id) => realm.Realm.Find<BeatmapSetInfo>(id); BeatmapSetInfo? fetchFromID(Guid id) => realm.Realm.Find<BeatmapSetInfo>(id);
} }
private void beatmapsChanged(IRealmCollection<BeatmapInfo> sender, ChangeSet? changes)
{
// we only care about actual changes in hidden status.
if (changes == null)
return;
bool changed = false;
foreach (int i in changes.InsertedIndices)
{
var beatmapInfo = sender[i];
var beatmapSet = beatmapInfo.BeatmapSet;
Debug.Assert(beatmapSet != null);
// Only require to action here if the beatmap is missing.
// This avoids processing these events unnecessarily when new beatmaps are imported, for example.
if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSets)
&& existingSets.SelectMany(s => s.Beatmaps).All(b => b.BeatmapInfo.ID != beatmapInfo.ID))
{
updateBeatmapSet(beatmapSet.Detach());
changed = true;
}
}
if (changed)
invalidateAfterChange();
}
public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
{ {
removeBeatmapSet(beatmapSet.ID); removeBeatmapSet(beatmapSet.ID);
@ -1292,12 +1253,5 @@ namespace osu.Game.Screens.Select
return ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding))); return ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding)));
} }
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
subscriptionBeatmaps?.Dispose();
}
} }
} }

View File

@ -1,8 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -14,6 +14,7 @@ using osu.Game.Configuration;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Localisation.SkinComponents; using osu.Game.Localisation.SkinComponents;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Utils;
using osuTK; using osuTK;
namespace osu.Game.Skinning namespace osu.Game.Skinning
@ -93,10 +94,10 @@ namespace osu.Game.Skinning
// but that requires further thought. // but that requires further thought.
var highestPrioritySkin = getHighestPriorityUserSkin(((SkinnableSprite)SettingSourceObject).source.AllSources) as Skin; var highestPrioritySkin = getHighestPriorityUserSkin(((SkinnableSprite)SettingSourceObject).source.AllSources) as Skin;
string[]? availableFiles = highestPrioritySkin?.SkinInfo.PerformRead(s => s.Files string[]? availableFiles = highestPrioritySkin?.SkinInfo.PerformRead(
.Where(f => f.Filename.EndsWith(".png", StringComparison.Ordinal) s => s.Files
|| f.Filename.EndsWith(".jpg", StringComparison.Ordinal)) .Where(f => SupportedExtensions.IMAGE_EXTENSIONS.Contains(Path.GetExtension(f.Filename).ToLowerInvariant()))
.Select(f => f.Filename).Distinct()).ToArray(); .Select(f => f.Filename).Distinct()).ToArray();
if (availableFiles?.Length > 0) if (availableFiles?.Length > 0)
Items = availableFiles; Items = availableFiles;

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Storyboards.Drawables; using osu.Game.Storyboards.Drawables;
using osu.Game.Utils;
namespace osu.Game.Storyboards namespace osu.Game.Storyboards
{ {
@ -96,8 +97,6 @@ namespace osu.Game.Storyboards
public virtual DrawableStoryboard CreateDrawable(IReadOnlyList<Mod>? mods = null) => public virtual DrawableStoryboard CreateDrawable(IReadOnlyList<Mod>? mods = null) =>
new DrawableStoryboard(this, mods); new DrawableStoryboard(this, mods);
private static readonly string[] image_extensions = { @".png", @".jpg" };
public virtual string? GetStoragePathFromStoryboardPath(string path) public virtual string? GetStoragePathFromStoryboardPath(string path)
{ {
string? resolvedPath = null; string? resolvedPath = null;
@ -109,7 +108,7 @@ namespace osu.Game.Storyboards
else else
{ {
// Some old storyboards don't include a file extension, so let's best guess at one. // Some old storyboards don't include a file extension, so let's best guess at one.
foreach (string ext in image_extensions) foreach (string ext in SupportedExtensions.IMAGE_EXTENSIONS)
{ {
if ((resolvedPath = BeatmapInfo.BeatmapSet?.GetPathForFile($"{path}{ext}")) != null) if ((resolvedPath = BeatmapInfo.BeatmapSet?.GetPathForFile($"{path}{ext}")) != null)
break; break;

View File

@ -78,12 +78,12 @@ namespace osu.Game.Tests.Visual.Spectator
/// <param name="state">The spectator state to end play with.</param> /// <param name="state">The spectator state to end play with.</param>
public void SendEndPlay(int userId, SpectatedUserState state = SpectatedUserState.Quit) public void SendEndPlay(int userId, SpectatedUserState state = SpectatedUserState.Quit)
{ {
if (!userBeatmapDictionary.ContainsKey(userId)) if (!userBeatmapDictionary.TryGetValue(userId, out int value))
return; return;
((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState
{ {
BeatmapID = userBeatmapDictionary[userId], BeatmapID = value,
RulesetID = 0, RulesetID = 0,
Mods = userModsDictionary[userId], Mods = userModsDictionary[userId],
State = state State = state

View File

@ -41,6 +41,12 @@ namespace osu.Game.Users
public virtual Color4 GetAppropriateColour(OsuColour colours) => colours.GreenDarker; public virtual Color4 GetAppropriateColour(OsuColour colours) => colours.GreenDarker;
/// <summary>
/// Returns the ID of the beatmap involved in this activity, if applicable and/or available.
/// </summary>
/// <param name="hideIdentifiableInformation"></param>
public virtual int? GetBeatmapID(bool hideIdentifiableInformation = false) => null;
[MessagePackObject] [MessagePackObject]
public class ChoosingBeatmap : UserActivity public class ChoosingBeatmap : UserActivity
{ {
@ -76,6 +82,7 @@ namespace osu.Game.Users
public override string GetStatus(bool hideIdentifiableInformation = false) => RulesetPlayingVerb; public override string GetStatus(bool hideIdentifiableInformation = false) => RulesetPlayingVerb;
public override string GetDetails(bool hideIdentifiableInformation = false) => BeatmapDisplayTitle; public override string GetDetails(bool hideIdentifiableInformation = false) => BeatmapDisplayTitle;
public override int? GetBeatmapID(bool hideIdentifiableInformation = false) => BeatmapID;
} }
[MessagePackObject] [MessagePackObject]
@ -156,6 +163,11 @@ namespace osu.Game.Users
// For now let's assume that showing the beatmap a user is editing could reveal unwanted information. // For now let's assume that showing the beatmap a user is editing could reveal unwanted information.
? string.Empty ? string.Empty
: BeatmapDisplayTitle; : BeatmapDisplayTitle;
public override int? GetBeatmapID(bool hideIdentifiableInformation = false) => hideIdentifiableInformation
// For now let's assume that showing the beatmap a user is editing could reveal unwanted information.
? null
: BeatmapID;
} }
[MessagePackObject] [MessagePackObject]

View File

@ -4,13 +4,16 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Online.API; using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osuTK; using osuTK;
namespace osu.Game.Users namespace osu.Game.Users
@ -24,13 +27,9 @@ namespace osu.Game.Users
private const int padding = 10; private const int padding = 10;
private const int main_content_height = 80; private const int main_content_height = 80;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private ProfileValueDisplay globalRankDisplay = null!; private ProfileValueDisplay globalRankDisplay = null!;
private ProfileValueDisplay countryRankDisplay = null!; private ProfileValueDisplay countryRankDisplay = null!;
private LoadingLayer loadingLayer = null!;
private readonly IBindable<UserStatistics?> statistics = new Bindable<UserStatistics?>();
public UserRankPanel(APIUser user) public UserRankPanel(APIUser user)
: base(user) : base(user)
@ -43,13 +42,37 @@ namespace osu.Game.Users
private void load() private void load()
{ {
BorderColour = ColourProvider?.Light1 ?? Colours.GreyVioletLighter; BorderColour = ColourProvider?.Light1 ?? Colours.GreyVioletLighter;
}
statistics.BindTo(api.Statistics); [Resolved]
statistics.BindValueChanged(stats => private LocalUserStatisticsProvider? statisticsProvider { get; set; }
{
globalRankDisplay.Content = stats.NewValue?.GlobalRank?.ToLocalisableString("\\##,##0") ?? "-"; [Resolved]
countryRankDisplay.Content = stats.NewValue?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-"; private IBindable<RulesetInfo> ruleset { get; set; } = null!;
}, true);
protected override void LoadComplete()
{
base.LoadComplete();
if (statisticsProvider != null)
statisticsProvider.StatisticsUpdated += onStatisticsUpdated;
ruleset.BindValueChanged(_ => updateDisplay(), true);
}
private void onStatisticsUpdated(UserStatisticsUpdate update)
{
if (update.Ruleset.Equals(ruleset.Value))
updateDisplay();
}
private void updateDisplay()
{
var statistics = statisticsProvider?.GetStatisticsFor(ruleset.Value);
loadingLayer.State.Value = statistics == null ? Visibility.Visible : Visibility.Hidden;
globalRankDisplay.Content = statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? "-";
countryRankDisplay.Content = statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-";
} }
protected override Drawable CreateLayout() protected override Drawable CreateLayout()
@ -176,7 +199,8 @@ namespace osu.Game.Users
} }
} }
} }
} },
loadingLayer = new LoadingLayer(true),
} }
}; };
@ -205,5 +229,13 @@ namespace osu.Game.Users
} }
protected override Drawable? CreateBackground() => null; protected override Drawable? CreateBackground() => null;
protected override void Dispose(bool isDisposing)
{
if (statisticsProvider.IsNotNull())
statisticsProvider.StatisticsUpdated -= onStatisticsUpdated;
base.Dispose(isDisposing);
}
} }
} }

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Utils
{
public static class SupportedExtensions
{
public static readonly string[] VIDEO_EXTENSIONS = [@".mp4", @".mov", @".avi", @".flv", @".mpg", @".wmv", @".m4v"];
public static readonly string[] AUDIO_EXTENSIONS = [@".mp3", @".ogg", @".wav"];
public static readonly string[] IMAGE_EXTENSIONS = [@".jpg", @".jpeg", @".png"];
public static readonly string[] ALL_EXTENSIONS =
[
..VIDEO_EXTENSIONS,
..AUDIO_EXTENSIONS,
..IMAGE_EXTENSIONS
];
}
}

View File

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

View File

@ -17,6 +17,6 @@
<MtouchInterpreter>-all</MtouchInterpreter> <MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.1118.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2024.1128.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>