1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-10 04:40:29 +08:00

Merge branch 'master' into fix-daily-challenge-leaderboard

This commit is contained in:
Bartłomiej Dach 2024-11-28 09:41:24 +01:00
commit 0d491e3159
No known key found for this signature in database
69 changed files with 1156 additions and 442 deletions

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

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
{ {
base.InitialiseDefaults(); base.InitialiseDefaults();
SetDefault(ManiaRulesetSetting.ScrollSpeed, 8, 1, 40); SetDefault(ManiaRulesetSetting.ScrollSpeed, 8.0, 1.0, 40.0, 0.1);
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false); SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
if (Get<double?>(ManiaRulesetSetting.ScrollTime) is double scrollTime) if (Get<double?>(ManiaRulesetSetting.ScrollTime) is double scrollTime)
{ {
SetValue(ManiaRulesetSetting.ScrollSpeed, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)); SetValue(ManiaRulesetSetting.ScrollSpeed, Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime));
SetValue<double?>(ManiaRulesetSetting.ScrollTime, null); SetValue<double?>(ManiaRulesetSetting.ScrollTime, null);
} }
#pragma warning restore CS0618 #pragma warning restore CS0618
@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{ {
new TrackedSetting<int>(ManiaRulesetSetting.ScrollSpeed, new TrackedSetting<double>(ManiaRulesetSetting.ScrollSpeed,
speed => new SettingDescription( speed => new SettingDescription(
rawValue: speed, rawValue: speed,
name: RulesetSettingsStrings.ScrollSpeed, name: RulesetSettingsStrings.ScrollSpeed,

View File

@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override void Update() protected override void Update()
{ {
TargetTimeRange = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get<int>(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value; TargetTimeRange = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get<double>(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value;
base.Update(); base.Update();
} }
} }

View File

@ -33,11 +33,11 @@ namespace osu.Game.Rulesets.Mania
LabelText = RulesetSettingsStrings.ScrollingDirection, LabelText = RulesetSettingsStrings.ScrollingDirection,
Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection) Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
}, },
new SettingsSlider<int, ManiaScrollSlider> new SettingsSlider<double, ManiaScrollSlider>
{ {
LabelText = RulesetSettingsStrings.ScrollSpeed, LabelText = RulesetSettingsStrings.ScrollSpeed,
Current = config.GetBindable<int>(ManiaRulesetSetting.ScrollSpeed), Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollSpeed),
KeyboardStep = 5 KeyboardStep = 1
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania
}; };
} }
private partial class ManiaScrollSlider : RoundedSliderBar<int> private partial class ManiaScrollSlider : RoundedSliderBar<double>
{ {
public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip((int)DrawableManiaRuleset.ComputeScrollTime(Current.Value), Current.Value); public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip((int)DrawableManiaRuleset.ComputeScrollTime(Current.Value), Current.Value);
} }

View File

@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.UI
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>(); private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
private readonly BindableInt configScrollSpeed = new BindableInt(); private readonly BindableDouble configScrollSpeed = new BindableDouble();
private double currentTimeRange; private double currentTimeRange;
protected double TargetTimeRange; protected double TargetTimeRange;
@ -160,7 +160,7 @@ namespace osu.Game.Rulesets.Mania.UI
/// </summary> /// </summary>
/// <param name="scrollSpeed">The scroll speed.</param> /// <param name="scrollSpeed">The scroll speed.</param>
/// <returns>The scroll time.</returns> /// <returns>The scroll time.</returns>
public static double ComputeScrollTime(int scrollSpeed) => MAX_TIME_RANGE / scrollSpeed; public static double ComputeScrollTime(double scrollSpeed) => MAX_TIME_RANGE / scrollSpeed;
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer();

View File

@ -38,6 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
MinValue = 0f, MinValue = 0f,
MaxValue = OsuPlayfield.BASE_SIZE.X, MaxValue = OsuPlayfield.BASE_SIZE.X,
Precision = 0.01f,
}; };
/// <summary> /// <summary>
@ -47,6 +48,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
MinValue = 0f, MinValue = 0f,
MaxValue = OsuPlayfield.BASE_SIZE.Y, MaxValue = OsuPlayfield.BASE_SIZE.Y,
Precision = 0.01f,
}; };
/// <summary> /// <summary>
@ -56,6 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
MinValue = 4f, MinValue = 4f,
MaxValue = 128f, MaxValue = 128f,
Precision = 0.01f,
}; };
/// <summary> /// <summary>
@ -65,6 +68,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
MinValue = -180f, MinValue = -180f,
MaxValue = 180f, MaxValue = 180f,
Precision = 0.01f,
}; };
/// <summary> /// <summary>

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

@ -1000,7 +1000,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(decoded.BeatmapInfo.WidescreenStoryboard, Is.False); Assert.That(decoded.BeatmapInfo.WidescreenStoryboard, Is.False);
Assert.That(decoded.BeatmapInfo.EpilepsyWarning, Is.False); Assert.That(decoded.BeatmapInfo.EpilepsyWarning, Is.False);
Assert.That(decoded.BeatmapInfo.SamplesMatchPlaybackRate, Is.False); Assert.That(decoded.BeatmapInfo.SamplesMatchPlaybackRate, Is.False);
Assert.That(decoded.BeatmapInfo.Countdown, Is.EqualTo(CountdownType.Normal)); Assert.That(decoded.BeatmapInfo.Countdown, Is.EqualTo(CountdownType.None));
Assert.That(decoded.BeatmapInfo.CountdownOffset, Is.EqualTo(0)); Assert.That(decoded.BeatmapInfo.CountdownOffset, Is.EqualTo(0));
Assert.That(decoded.BeatmapInfo.Metadata.PreviewTime, Is.EqualTo(-1)); Assert.That(decoded.BeatmapInfo.Metadata.PreviewTime, Is.EqualTo(-1));
Assert.That(decoded.BeatmapInfo.Ruleset.OnlineID, Is.EqualTo(0)); Assert.That(decoded.BeatmapInfo.Ruleset.OnlineID, Is.EqualTo(0));

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

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

@ -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.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -58,6 +59,16 @@ namespace osu.Game.Tests.Visual.Online
return true; return true;
} }
if (req is GetUserBeatmapsRequest getUserBeatmapsRequest)
{
getUserBeatmapsRequest.TriggerSuccess(new List<APIBeatmapSet>
{
CreateAPIBeatmapSet(),
CreateAPIBeatmapSet()
});
return true;
}
return false; return false;
}; };
}); });

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

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

@ -150,7 +150,7 @@ namespace osu.Game.Beatmaps
public bool EpilepsyWarning { get; set; } public bool EpilepsyWarning { get; set; }
public bool SamplesMatchPlaybackRate { get; set; } = true; public bool SamplesMatchPlaybackRate { get; set; }
/// <summary> /// <summary>
/// The time at which this beatmap was last played by the local user. /// The time at which this beatmap was last played by the local user.
@ -181,7 +181,7 @@ namespace osu.Game.Beatmaps
public double? EditorTimestamp { get; set; } public double? EditorTimestamp { get; set; }
[Ignored] [Ignored]
public CountdownType Countdown { get; set; } = CountdownType.Normal; public CountdownType Countdown { get; set; } = CountdownType.None;
/// <summary> /// <summary>
/// The number of beats to move the countdown backwards (compared to its default location). /// The number of beats to move the countdown backwards (compared to its default location).

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

@ -86,7 +86,7 @@ namespace osu.Game.Beatmaps.Drawables
}; };
Status = BeatmapOnlineStatus.None; Status = BeatmapOnlineStatus.None;
TextPadding = new MarginPadding { Horizontal = 5, Bottom = 1 }; TextPadding = new MarginPadding { Horizontal = 4, Bottom = 1 };
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -20,9 +20,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards
public abstract partial class BeatmapCard : OsuClickableContainer, IHasContextMenu public abstract partial class BeatmapCard : OsuClickableContainer, IHasContextMenu
{ {
public const float TRANSITION_DURATION = 340; public const float TRANSITION_DURATION = 340;
public const float CORNER_RADIUS = 10; public const float CORNER_RADIUS = 8;
protected const float WIDTH = 430; protected const float WIDTH = 345;
public IBindable<bool> Expanded { get; } public IBindable<bool> Expanded { get; }

View File

@ -22,7 +22,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
protected override Drawable IdleContent => idleBottomContent; protected override Drawable IdleContent => idleBottomContent;
protected override Drawable DownloadInProgressContent => downloadProgressBar; protected override Drawable DownloadInProgressContent => downloadProgressBar;
private const float height = 140; private const float height = 112;
[Cached] [Cached]
private readonly BeatmapCardContent content; private readonly BeatmapCardContent content;
@ -68,7 +68,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Padding = new MarginPadding { Right = CORNER_RADIUS }, Padding = new MarginPadding { Right = CORNER_RADIUS },
Child = leftIconArea = new FillFlowContainer Child = leftIconArea = new FillFlowContainer
{ {
Margin = new MarginPadding(5), Margin = new MarginPadding(4),
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(1) Spacing = new Vector2(1)
@ -80,7 +80,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Width = WIDTH - height + CORNER_RADIUS, Width = WIDTH - height + CORNER_RADIUS,
FavouriteState = { BindTarget = FavouriteState }, FavouriteState = { BindTarget = FavouriteState },
ButtonsCollapsedWidth = CORNER_RADIUS, ButtonsCollapsedWidth = CORNER_RADIUS,
ButtonsExpandedWidth = 30, ButtonsExpandedWidth = 24,
Children = new Drawable[] Children = new Drawable[]
{ {
new FillFlowContainer new FillFlowContainer
@ -109,7 +109,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
new TruncatingSpriteText new TruncatingSpriteText
{ {
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
}, },
titleBadgeArea = new FillFlowContainer titleBadgeArea = new FillFlowContainer
@ -142,7 +142,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
new TruncatingSpriteText new TruncatingSpriteText
{ {
Text = createArtistText(), Text = createArtistText(),
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
}, },
Empty() Empty()
@ -154,7 +154,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Text = BeatmapSet.Source, Text = BeatmapSet.Source,
Shadow = false, Shadow = false,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold),
Colour = colourProvider.Content2 Colour = colourProvider.Content2
}, },
} }
@ -173,18 +173,18 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 3), Spacing = new Vector2(0, 2),
AlwaysPresent = true, AlwaysPresent = true,
Children = new Drawable[] Children = new Drawable[]
{ {
new LinkFlowContainer(s => new LinkFlowContainer(s =>
{ {
s.Shadow = false; s.Shadow = false;
s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold); s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold);
}).With(d => }).With(d =>
{ {
d.AutoSizeAxes = Axes.Both; d.AutoSizeAxes = Axes.Both;
d.Margin = new MarginPadding { Top = 2 }; d.Margin = new MarginPadding { Top = 1 };
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
d.AddUserLink(BeatmapSet.Author); d.AddUserLink(BeatmapSet.Author);
}), }),
@ -215,7 +215,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
downloadProgressBar = new BeatmapCardDownloadProgressBar downloadProgressBar = new BeatmapCardDownloadProgressBar
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 6, Height = 5,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
State = { BindTarget = DownloadTracker.State }, State = { BindTarget = DownloadTracker.State },
@ -231,17 +231,17 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, Padding = new MarginPadding { Horizontal = 8, Vertical = 10 },
Child = new BeatmapCardDifficultyList(BeatmapSet) Child = new BeatmapCardDifficultyList(BeatmapSet)
}; };
c.Expanded.BindTarget = Expanded; c.Expanded.BindTarget = Expanded;
}); });
if (BeatmapSet.HasVideo) if (BeatmapSet.HasVideo)
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(20) }); leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
if (BeatmapSet.HasStoryboard) if (BeatmapSet.HasStoryboard)
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) }); leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
if (BeatmapSet.FeaturedInSpotlight) if (BeatmapSet.FeaturedInSpotlight)
{ {
@ -249,7 +249,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 5 } Margin = new MarginPadding { Left = 4 }
}); });
} }
@ -259,7 +259,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 5 } Margin = new MarginPadding { Left = 4 }
}); });
} }
@ -269,7 +269,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 5 } Margin = new MarginPadding { Left = 4 }
}; };
} }
@ -288,7 +288,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
BeatmapCardStatistic withMargin(BeatmapCardStatistic original) BeatmapCardStatistic withMargin(BeatmapCardStatistic original)
{ {
original.Margin = new MarginPadding { Right = 10 }; original.Margin = new MarginPadding { Right = 8 };
return original; return original;
} }

View File

@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(4, 0), Spacing = new Vector2(3, 0),
Children = new Drawable[] Children = new Drawable[]
{ {
new BeatmapSetOnlineStatusPill new BeatmapSetOnlineStatusPill
@ -33,13 +33,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Status = beatmapSet.Status, Status = beatmapSet.Status,
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft Origin = Anchor.CentreLeft,
TextSize = 13f
}, },
new DifficultySpectrumDisplay(beatmapSet) new DifficultySpectrumDisplay(beatmapSet)
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
DotSize = new Vector2(6, 12) DotSize = new Vector2(5, 10)
} }
} }
}; };

View File

@ -23,7 +23,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
protected override Drawable IdleContent => idleBottomContent; protected override Drawable IdleContent => idleBottomContent;
protected override Drawable DownloadInProgressContent => downloadProgressBar; protected override Drawable DownloadInProgressContent => downloadProgressBar;
public const float HEIGHT = 100; public const float HEIGHT = 80;
[Cached] [Cached]
private readonly BeatmapCardContent content; private readonly BeatmapCardContent content;
@ -69,7 +69,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Padding = new MarginPadding { Right = CORNER_RADIUS }, Padding = new MarginPadding { Right = CORNER_RADIUS },
Child = leftIconArea = new FillFlowContainer Child = leftIconArea = new FillFlowContainer
{ {
Margin = new MarginPadding(5), Margin = new MarginPadding(4),
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(1) Spacing = new Vector2(1)
@ -81,7 +81,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Width = WIDTH - HEIGHT + CORNER_RADIUS, Width = WIDTH - HEIGHT + CORNER_RADIUS,
FavouriteState = { BindTarget = FavouriteState }, FavouriteState = { BindTarget = FavouriteState },
ButtonsCollapsedWidth = CORNER_RADIUS, ButtonsCollapsedWidth = CORNER_RADIUS,
ButtonsExpandedWidth = 30, ButtonsExpandedWidth = 24,
Children = new Drawable[] Children = new Drawable[]
{ {
new FillFlowContainer new FillFlowContainer
@ -110,7 +110,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
new TruncatingSpriteText new TruncatingSpriteText
{ {
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
}, },
titleBadgeArea = new FillFlowContainer titleBadgeArea = new FillFlowContainer
@ -143,7 +143,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
new TruncatingSpriteText new TruncatingSpriteText
{ {
Text = createArtistText(), Text = createArtistText(),
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
}, },
Empty() Empty()
@ -153,11 +153,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
new LinkFlowContainer(s => new LinkFlowContainer(s =>
{ {
s.Shadow = false; s.Shadow = false;
s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold); s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold);
}).With(d => }).With(d =>
{ {
d.AutoSizeAxes = Axes.Both; d.AutoSizeAxes = Axes.Both;
d.Margin = new MarginPadding { Top = 2 }; d.Margin = new MarginPadding { Top = 1 };
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
d.AddUserLink(BeatmapSet.Author); d.AddUserLink(BeatmapSet.Author);
}), }),
@ -177,7 +177,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 3), Spacing = new Vector2(0, 2),
AlwaysPresent = true, AlwaysPresent = true,
Children = new Drawable[] Children = new Drawable[]
{ {
@ -186,7 +186,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0), Spacing = new Vector2(8, 0),
Alpha = 0, Alpha = 0,
AlwaysPresent = true, AlwaysPresent = true,
ChildrenEnumerable = createStatistics() ChildrenEnumerable = createStatistics()
@ -197,7 +197,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
downloadProgressBar = new BeatmapCardDownloadProgressBar downloadProgressBar = new BeatmapCardDownloadProgressBar
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 6, Height = 5,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
State = { BindTarget = DownloadTracker.State }, State = { BindTarget = DownloadTracker.State },
@ -213,17 +213,17 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, Padding = new MarginPadding { Horizontal = 8, Vertical = 10 },
Child = new BeatmapCardDifficultyList(BeatmapSet) Child = new BeatmapCardDifficultyList(BeatmapSet)
}; };
c.Expanded.BindTarget = Expanded; c.Expanded.BindTarget = Expanded;
}); });
if (BeatmapSet.HasVideo) if (BeatmapSet.HasVideo)
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(20) }); leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
if (BeatmapSet.HasStoryboard) if (BeatmapSet.HasStoryboard)
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) }); leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
if (BeatmapSet.FeaturedInSpotlight) if (BeatmapSet.FeaturedInSpotlight)
{ {
@ -231,7 +231,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 5 } Margin = new MarginPadding { Left = 4 }
}); });
} }
@ -241,7 +241,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 5 } Margin = new MarginPadding { Left = 4 }
}); });
} }
@ -251,7 +251,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 5 } Margin = new MarginPadding { Left = 4 }
}; };
} }
} }

View File

@ -46,21 +46,21 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0), Spacing = new Vector2(4, 0),
Children = new Drawable[] Children = new Drawable[]
{ {
spriteIcon = new SpriteIcon spriteIcon = new SpriteIcon
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Size = new Vector2(10), Size = new Vector2(8),
Margin = new MarginPadding { Top = 1 } Margin = new MarginPadding { Top = 1 }
}, },
spriteText = new OsuSpriteText spriteText = new OsuSpriteText
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Font = OsuFont.Default.With(size: 14) Font = OsuFont.Default.With(size: 11)
} }
} }
}; };

View File

@ -192,7 +192,6 @@ namespace osu.Game.Beatmaps.Formats
private static void applyLegacyDefaults(BeatmapInfo beatmapInfo) private static void applyLegacyDefaults(BeatmapInfo beatmapInfo)
{ {
beatmapInfo.WidescreenStoryboard = false; beatmapInfo.WidescreenStoryboard = false;
beatmapInfo.SamplesMatchPlaybackRate = false;
} }
protected override void ParseLine(Beatmap beatmap, Section section, string line) protected override void ParseLine(Beatmap beatmap, Section section, string line)

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);
@ -214,6 +215,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.EditorContractSidebars, false); SetDefault(OsuSetting.EditorContractSidebars, false);
SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false); SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false);
SetDefault(OsuSetting.AlwaysRequireHoldingForPause, false);
} }
protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup) protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup)
@ -413,6 +415,7 @@ namespace osu.Game.Configuration
NotifyOnPrivateMessage, NotifyOnPrivateMessage,
UIHoldActivationDelay, UIHoldActivationDelay,
HitLighting, HitLighting,
StarFountains,
MenuBackgroundSource, MenuBackgroundSource,
GameplayDisableWinKey, GameplayDisableWinKey,
SeasonalBackgroundMode, SeasonalBackgroundMode,
@ -444,5 +447,6 @@ namespace osu.Game.Configuration
EditorRotationOrigin, EditorRotationOrigin,
EditorTimelineShowBreaks, EditorTimelineShowBreaks,
EditorAdjustExistingObjectsOnTimingChanges, EditorAdjustExistingObjectsOnTimingChanges,
AlwaysRequireHoldingForPause
} }
} }

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>
@ -89,6 +94,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString AlwaysShowHoldForMenuButton => new TranslatableString(getKey(@"always_show_hold_for_menu_button"), @"Always show hold for menu button"); public static LocalisableString AlwaysShowHoldForMenuButton => new TranslatableString(getKey(@"always_show_hold_for_menu_button"), @"Always show hold for menu button");
/// <summary>
/// "Require holding key to pause gameplay"
/// </summary>
public static LocalisableString AlwaysRequireHoldForMenu => new TranslatableString(getKey(@"require_holding_key_to_pause_gameplay"), @"Require holding key to pause gameplay");
/// <summary> /// <summary>
/// "Always play first combo break sound" /// "Always play first combo break sound"
/// </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

@ -80,9 +80,9 @@ namespace osu.Game.Localisation
public static LocalisableString TimingBasedColouring => new TranslatableString(getKey(@"Timing_based_colouring"), @"Timing-based note colouring"); public static LocalisableString TimingBasedColouring => new TranslatableString(getKey(@"Timing_based_colouring"), @"Timing-based note colouring");
/// <summary> /// <summary>
/// "{0}ms (speed {1})" /// "{0}ms (speed {1:N1})"
/// </summary> /// </summary>
public static LocalisableString ScrollSpeedTooltip(int scrollTime, int scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1})", scrollTime, scrollSpeed); public static LocalisableString ScrollSpeedTooltip(int scrollTime, double scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1:N1})", scrollTime, scrollSpeed);
/// <summary> /// <summary>
/// "Touch control scheme" /// "Touch control scheme"

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

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

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

@ -366,12 +366,8 @@ namespace osu.Game.Online.Rooms
{ {
RoomID = other.RoomID; RoomID = other.RoomID;
Name = other.Name; Name = other.Name;
Category = other.Category; Category = other.Category;
Host = other.Host;
if (other.Host != null && Host?.Id != other.Host.Id)
Host = other.Host;
ChannelId = other.ChannelId; ChannelId = other.ChannelId;
Status = other.Status; Status = other.Status;
Availability = other.Availability; Availability = other.Availability;
@ -387,22 +383,10 @@ namespace osu.Game.Online.Rooms
PlaylistItemStats = other.PlaylistItemStats; PlaylistItemStats = other.PlaylistItemStats;
CurrentPlaylistItem = other.CurrentPlaylistItem; CurrentPlaylistItem = other.CurrentPlaylistItem;
AutoSkip = other.AutoSkip; AutoSkip = other.AutoSkip;
other.RemoveExpiredPlaylistItems();
Playlist = other.Playlist; Playlist = other.Playlist;
RecentParticipants = other.RecentParticipants; RecentParticipants = other.RecentParticipants;
} }
public void RemoveExpiredPlaylistItems()
{
// Todo: This is not the best way/place to do this, but the intention is to display all playlist items when the room has ended,
// and display only the non-expired playlist items while the room is still active. In order to achieve this, all expired items are removed from the source Room.
// More refactoring is required before this can be done locally instead - DrawableRoomPlaylist is currently directly bound to the playlist to display items in the room.
if (Status is not RoomStatusEnded)
Playlist = Playlist.Where(i => !i.Expired).ToArray();
}
[JsonObject(MemberSerialization.OptIn)] [JsonObject(MemberSerialization.OptIn)]
public class RoomPlaylistItemStats public class RoomPlaylistItemStats
{ {

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();
@ -1069,7 +1068,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 +1142,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));

View File

@ -198,7 +198,6 @@ namespace osu.Game.Overlays
{ {
c.Anchor = Anchor.TopCentre; c.Anchor = Anchor.TopCentre;
c.Origin = Anchor.TopCentre; c.Origin = Anchor.TopCentre;
c.Scale = new Vector2(0.8f);
})).ToArray(); })).ToArray();
private static ReverseChildIDFillFlowContainer<BeatmapCard> createCardContainerFor(IEnumerable<BeatmapCard> newCards) private static ReverseChildIDFillFlowContainer<BeatmapCard> createCardContainerFor(IEnumerable<BeatmapCard> newCards)

View File

@ -71,7 +71,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
? new BeatmapCardNormal(model) ? new BeatmapCardNormal(model)
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre
} }
: null; : null;
} }

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

@ -41,6 +41,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
Current = config.GetBindable<bool>(OsuSetting.GameplayLeaderboard), Current = config.GetBindable<bool>(OsuSetting.GameplayLeaderboard),
}, },
new SettingsCheckbox new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.AlwaysRequireHoldForMenu,
Current = config.GetBindable<bool>(OsuSetting.AlwaysRequireHoldingForPause),
},
new SettingsCheckbox
{ {
LabelText = GameplaySettingsStrings.AlwaysShowHoldForMenuButton, LabelText = GameplaySettingsStrings.AlwaysShowHoldForMenuButton,
Current = config.GetBindable<bool>(OsuSetting.AlwaysShowHoldForMenuButton), Current = config.GetBindable<bool>(OsuSetting.AlwaysShowHoldForMenuButton),

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

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

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

@ -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,50 +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,
"Press Ctrl-Shift-F to toggle the FPS Counter. But make sure not to pay too much attention to it!", MenuTipStrings.ToggleReplaySettingsShortcut,
"While watching a replay, press Ctrl-H to toggle replay settings!", MenuTipStrings.CopyModsFromScore,
"You can easily copy the mods from scores on a leaderboard by right-clicking on them!", MenuTipStrings.AutoplayBeatmapShortcut
"Ctrl-Enter at song select will start a beatmap in autoplay mode!"
}; };
return tips[RNG.Next(0, tips.Length)]; return tips[RNG.Next(0, tips.Length)];

View File

@ -8,6 +8,9 @@ using osu.Game.Graphics.Sprites;
using osuTK; using osuTK;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -27,26 +30,60 @@ namespace osu.Game.Screens.Menu
public SongTicker() public SongTicker()
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
Child = new FillFlowContainer InternalChildren = new Drawable[]
{ {
AutoSizeAxes = Axes.Both, new Container
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 3),
Children = new Drawable[]
{ {
title = new OsuSpriteText Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Position = new Vector2(5, -5),
Padding = new MarginPadding(-5),
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{ {
Anchor = Anchor.TopRight, new CircularContainer
Origin = Anchor.TopRight, {
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light, italics: true) RelativeSizeAxes = Axes.Both,
}, Masking = true,
artist = new OsuSpriteText EdgeEffect = new EdgeEffectParameters
{ {
Anchor = Anchor.TopRight, Radius = 75,
Origin = Anchor.TopRight, Type = EdgeEffectType.Shadow,
Font = OsuFont.GetFont(size: 16) Colour = OsuColour.Gray(0.04f).Opacity(0.3f),
},
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
AlwaysPresent = true,
Alpha = 0,
},
}
},
} }
} },
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 3),
Children = new Drawable[]
{
title = new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light, italics: true)
},
artist = new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Font = OsuFont.GetFont(size: 16)
}
}
},
}; };
} }

View File

@ -60,10 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
} }
foreach (var incoming in result) foreach (var incoming in result)
{
incoming.RemoveExpiredPlaylistItems();
RoomManager.AddOrUpdateRoom(incoming); RoomManager.AddOrUpdateRoom(incoming);
}
initialRoomsReceived.Value = true; initialRoomsReceived.Value = true;
tcs.SetResult(true); tcs.SetResult(true);

View File

@ -36,7 +36,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
req.Success += result => req.Success += result =>
{ {
result.RemoveExpiredPlaylistItems();
RoomManager.AddOrUpdateRoom(result); RoomManager.AddOrUpdateRoom(result);
tcs.SetResult(true); tcs.SetResult(true);
}; };

View File

@ -438,7 +438,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
=> MaxParticipantsField.Text = room.MaxParticipants?.ToString(); => MaxParticipantsField.Text = room.MaxParticipants?.ToString();
private void updateRoomAutoStartDuration() private void updateRoomAutoStartDuration()
=> typeLabel.Text = room.AutoStartDuration.GetLocalisableDescription(); => startModeDropdown.Current.Value = (StartMode)room.AutoStartDuration.TotalSeconds;
private void updateRoomPlaylist() private void updateRoomPlaylist()
=> drawablePlaylist.Items.ReplaceRange(0, drawablePlaylist.Items.Count, room.Playlist); => drawablePlaylist.Items.ReplaceRange(0, drawablePlaylist.Items.Count, room.Playlist);

View File

@ -162,14 +162,18 @@ namespace osu.Game.Screens.Play.HUD
private bool pendingAnimation; private bool pendingAnimation;
private ScheduledDelegate shakeOperation; private ScheduledDelegate shakeOperation;
private Bindable<bool> alwaysRequireHold;
public HoldButton(bool isDangerousAction) public HoldButton(bool isDangerousAction)
: base(isDangerousAction) : base(isDangerousAction)
{ {
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours, OsuConfigManager config)
{ {
alwaysRequireHold = config.GetBindable<bool>(OsuSetting.AlwaysRequireHoldingForPause);
Size = new Vector2(60); Size = new Vector2(60);
Child = new CircularContainer Child = new CircularContainer
@ -299,7 +303,13 @@ namespace osu.Game.Screens.Play.HUD
{ {
case GlobalAction.Back: case GlobalAction.Back:
if (!pendingAnimation) if (!pendingAnimation)
Confirm(); {
if (IsDangerousAction || alwaysRequireHold.Value)
BeginConfirm();
else
Confirm();
}
return true; return true;
case GlobalAction.PauseGameplay: case GlobalAction.PauseGameplay:
@ -307,7 +317,13 @@ namespace osu.Game.Screens.Play.HUD
if (ReplayLoaded.Value) return false; if (ReplayLoaded.Value) return false;
if (!pendingAnimation) if (!pendingAnimation)
Confirm(); {
if (IsDangerousAction || alwaysRequireHold.Value)
BeginConfirm();
else
Confirm();
}
return true; return true;
} }

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

@ -976,7 +976,9 @@ namespace osu.Game.Screens.Play
if (PauseOverlay.State.Value == Visibility.Visible) if (PauseOverlay.State.Value == Visibility.Visible)
PauseOverlay.Hide(); PauseOverlay.Hide();
failAnimationContainer.Start(); bool restartOnFail = GameplayState.Mods.OfType<IApplicableFailOverride>().Any(m => m.RestartOnFail);
if (!restartOnFail)
failAnimationContainer.Start();
// Failures can be triggered either by a judgement, or by a mod. // Failures can be triggered either by a judgement, or by a mod.
// //
@ -990,7 +992,7 @@ namespace osu.Game.Screens.Play
ScoreProcessor.FailScore(Score.ScoreInfo); ScoreProcessor.FailScore(Score.ScoreInfo);
OnFail(); OnFail();
if (GameplayState.Mods.OfType<IApplicableFailOverride>().Any(m => m.RestartOnFail)) if (restartOnFail)
Restart(true); Restart(true);
}); });
} }

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

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