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

Merge branch 'master' into close-playlists

This commit is contained in:
Bartłomiej Dach 2024-11-28 14:01:36 +01:00
commit 2e6f43a75d
No known key found for this signature in database
94 changed files with 1511 additions and 631 deletions

View File

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

View File

@ -4,28 +4,54 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
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;
namespace osu.Game.Benchmarks
{
public class BenchmarkUnstableRate : BenchmarkTest
{
private List<HitEvent> events = null!;
private readonly List<List<HitEvent>> incrementalEventLists = new List<List<HitEvent>>();
public override void SetUp()
{
base.SetUp();
events = new List<HitEvent>();
for (int i = 0; i < 1000; i++)
events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, new HitObject(), null, null));
var events = new List<HitEvent>();
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]
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();
SetDefault(ManiaRulesetSetting.ScrollSpeed, 8, 1, 40);
SetDefault(ManiaRulesetSetting.ScrollSpeed, 8.0, 1.0, 40.0, 0.1);
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
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);
}
#pragma warning restore CS0618
@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{
new TrackedSetting<int>(ManiaRulesetSetting.ScrollSpeed,
new TrackedSetting<double>(ManiaRulesetSetting.ScrollSpeed,
speed => new SettingDescription(
rawValue: speed,
name: RulesetSettingsStrings.ScrollSpeed,

View File

@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.Edit
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();
}
}

View File

@ -33,11 +33,11 @@ namespace osu.Game.Rulesets.Mania
LabelText = RulesetSettingsStrings.ScrollingDirection,
Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
},
new SettingsSlider<int, ManiaScrollSlider>
new SettingsSlider<double, ManiaScrollSlider>
{
LabelText = RulesetSettingsStrings.ScrollSpeed,
Current = config.GetBindable<int>(ManiaRulesetSetting.ScrollSpeed),
KeyboardStep = 5
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollSpeed),
KeyboardStep = 1
},
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);
}

View File

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

View File

@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.UI
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
private readonly BindableInt configScrollSpeed = new BindableInt();
private readonly BindableDouble configScrollSpeed = new BindableDouble();
private double currentTimeRange;
protected double TargetTimeRange;
@ -160,7 +160,7 @@ namespace osu.Game.Rulesets.Mania.UI
/// </summary>
/// <param name="scrollSpeed">The scroll speed.</param>
/// <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();

View File

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

View File

@ -63,18 +63,18 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-top"),
},
fixedMiddle = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-middle"),
},
spinningMiddle = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
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.EpilepsyWarning, 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.Metadata.PreviewTime, Is.EqualTo(-1));
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.
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
@ -64,6 +65,10 @@ namespace osu.Game.Tests
// Beatmap must be imported before the collection manager is loaded.
if (withBeatmap)
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()
{
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);
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]

View File

@ -527,8 +527,11 @@ namespace osu.Game.Tests.Visual.Editing
checkPlacementSampleBank(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 checkPlacementSampleAdditionBank(string expected) => AddAssert($"Placement sample addition 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}",
() => 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]
@ -781,15 +784,39 @@ namespace osu.Game.Tests.Visual.Editing
setAdditionBankViaPopover(HitSampleInfo.BANK_SOFT);
dismissPopover();
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
assertNoChanges();
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);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
AddStep("select second object", () =>
{
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", () =>
@ -883,11 +910,12 @@ namespace osu.Game.Tests.Visual.Editing
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}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
return h is not null && h.NodeSamples[nodeIndex].All(o => o.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);
});
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);
});
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);
});
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);
});
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);
});
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);
});
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);
});
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);
});
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);
});
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);
});
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.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays;
using osu.Game.Overlays.Login;
using osu.Game.Overlays.Settings;
using osu.Game.Tests.Visual.Online;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osuTK.Input;
@ -31,6 +33,9 @@ namespace osu.Game.Tests.Visual.Menus
[Resolved]
private OsuConfigManager configManager { get; set; } = null!;
[Cached(typeof(LocalUserStatisticsProvider))]
private readonly TestSceneUserPanel.TestUserStatisticsProvider statisticsProvider = new TestSceneUserPanel.TestUserStatisticsProvider();
[BackgroundDependencyLoader]
private void load()
{
@ -170,6 +175,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "88800088");
assertAPIState(APIState.Online);
AddStep("feed statistics", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value));
AddStep("click on flag", () =>
{
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<UpdateableFlag>().First());

View File

@ -3,8 +3,10 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
@ -73,5 +75,57 @@ namespace osu.Game.Tests.Visual.Menus
((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", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(),
new UserStatistics
{
@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Loss", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(),
new UserStatistics
{
@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Tiny increase in PP", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(),
new UserStatistics
{
@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("No change 1", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(),
new UserStatistics
{
@ -170,7 +170,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Was null", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(),
new UserStatistics
{
@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Became null", () =>
{
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(),
new UserStatistics
{

View File

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

View File

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

View File

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

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
@ -11,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Rulesets;
@ -24,17 +23,20 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture]
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 UserGridPanel boundPanel1;
private TestUserListPanel boundPanel2;
private UserGridPanel boundPanel1 = null!;
private TestUserListPanel boundPanel2 = null!;
[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]
private IRulesetStore rulesetStore { get; set; }
private IRulesetStore rulesetStore { get; set; } = null!;
[SetUp]
public void SetUp() => Schedule(() =>
@ -42,7 +44,11 @@ namespace osu.Game.Tests.Visual.Online
activity.Value = null;
status.Value = null;
Child = new FillFlowContainer
Remove(statisticsProvider, false);
Clear();
Add(statisticsProvider);
Add(new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -108,7 +114,7 @@ namespace osu.Game.Tests.Visual.Online
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
}) { Width = 300 }
}
};
});
boundPanel1.Status.BindTo(status);
boundPanel1.Activity.BindTo(activity);
@ -162,24 +168,21 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("update statistics", () =>
{
API.UpdateStatistics(new UserStatistics
statisticsProvider.UpdateStatistics(new UserStatistics
{
GlobalRank = RNG.Next(100000),
CountryRank = RNG.Next(100000)
});
}, Ruleset.Value);
});
AddStep("set statistics to something big", () =>
{
API.UpdateStatistics(new UserStatistics
statisticsProvider.UpdateStatistics(new UserStatistics
{
GlobalRank = RNG.Next(1_000_000, 100_000_000),
CountryRank = RNG.Next(1_000_000, 100_000_000)
});
});
AddStep("set statistics to empty", () =>
{
API.UpdateStatistics(new UserStatistics());
}, Ruleset.Value);
});
AddStep("set statistics to empty", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value));
}
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 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.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
@ -58,6 +59,16 @@ namespace osu.Game.Tests.Visual.Online
return true;
}
if (req is GetUserBeatmapsRequest getUserBeatmapsRequest)
{
getUserBeatmapsRequest.TriggerSuccess(new List<APIBeatmapSet>
{
CreateAPIBeatmapSet(),
CreateAPIBeatmapSet()
});
return true;
}
return false;
};
});

View File

@ -25,6 +25,7 @@ namespace osu.Game.Tests.Visual.Online
{
protected override bool UseOnlineAPI => false;
private LocalUserStatisticsProvider statisticsProvider = null!;
private UserStatisticsWatcher watcher = null!;
[Resolved]
@ -107,7 +108,9 @@ namespace osu.Game.Tests.Visual.Online
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;
UserStatisticsUpdate? update = null;
ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
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.
feignScoreProcessing(userId, ruleset, 5_000_000);
UserStatisticsUpdate? update = null;
ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
@ -164,7 +167,7 @@ namespace osu.Game.Tests.Visual.Online
long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null;
ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000);
@ -191,7 +194,7 @@ namespace osu.Game.Tests.Visual.Online
long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null;
ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000);
@ -212,7 +215,7 @@ namespace osu.Game.Tests.Visual.Online
long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null;
ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000);
@ -241,7 +244,7 @@ namespace osu.Game.Tests.Visual.Online
feignScoreProcessing(userId, ruleset, 6_000_000);
UserStatisticsUpdate? update = null;
ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(secondScoreId, ruleset, receivedUpdate => update = receivedUpdate);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, secondScoreId));
@ -259,15 +262,14 @@ namespace osu.Game.Tests.Visual.Online
var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null;
ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
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", () => dummyAPI.Statistics.Value!.TotalScore, () => Is.EqualTo(5_000_000));
AddAssert("statistics values are correct", () => statisticsProvider.GetStatisticsFor(ruleset)!.TotalScore, () => Is.EqualTo(5_000_000));
}
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", () =>
{
watcher.RegisterForStatisticsUpdateAfter(

View File

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

View File

@ -112,6 +112,6 @@ namespace osu.Game.Tests.Visual.Ranking
});
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!;
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", () =>
{
score = TestResources.CreateTestScoreInfo();
score.OnlineID = 1234;
((Bindable<UserStatisticsUpdate>)userStatisticsWatcher.LatestUpdate).Value = new UserStatisticsUpdate(score,
((Bindable<ScoreBasedUserStatisticsUpdate>)userStatisticsWatcher.LatestUpdate).Value = new ScoreBasedUserStatisticsUpdate(score,
new UserStatistics
{
Level = new UserStatistics.LevelInfo
@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Ranking
Score = { Value = score },
DisplayedUserStatisticsUpdate =
{
Value = new UserStatisticsUpdate(score, new UserStatistics
Value = new ScoreBasedUserStatisticsUpdate(score, new UserStatistics
{
Level = new UserStatistics.LevelInfo
{

View File

@ -8,14 +8,14 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Extensions;
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;
@ -28,25 +28,31 @@ namespace osu.Game.Tests.Visual.SongSelect
{
public partial class TestSceneBeatmapRecommendations : OsuGameTestScene
{
[Resolved]
private IRulesetStore rulesetStore { get; set; }
[SetUpSteps]
public override void SetUpSteps()
{
AddStep("populate ruleset statistics", () =>
{
Dictionary<string, UserStatistics> rulesetStatistics = new Dictionary<string, UserStatistics>();
rulesetStore.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo =>
((DummyAPIAccess)API).HandleRequest = r =>
{
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)

View File

@ -150,7 +150,7 @@ namespace osu.Game.Beatmaps
public bool EpilepsyWarning { get; set; }
public bool SamplesMatchPlaybackRate { get; set; } = true;
public bool SamplesMatchPlaybackRate { get; set; }
/// <summary>
/// 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; }
[Ignored]
public CountdownType Countdown { get; set; } = CountdownType.Normal;
public CountdownType Countdown { get; set; } = CountdownType.None;
/// <summary>
/// The number of beats to move the countdown backwards (compared to its default location).

View File

@ -559,7 +559,11 @@ namespace osu.Game.Beatmaps
// If we seem to be missing files, now is a good time to re-fetch.
bool missingFiles = beatmapInfo.BeatmapSet?.Files.Count == 0;
if (refetch || beatmapInfo.IsManaged || missingFiles)
if (beatmapInfo.IsManaged)
{
beatmapInfo = beatmapInfo.Detach();
}
else if (refetch || missingFiles)
{
Guid id = beatmapInfo.ID;
beatmapInfo = Realm.Run(r => r.Find<BeatmapInfo>(id)?.Detach()) ?? beatmapInfo;

View File

@ -9,9 +9,11 @@ using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Game.Online.API;
using osu.Game.Online;
using osu.Game.Rulesets;
using osu.Game.Users;
namespace osu.Game.Beatmaps
{
@ -21,18 +23,63 @@ namespace osu.Game.Beatmaps
/// </summary>
public partial class DifficultyRecommender : Component
{
[Resolved]
private IAPIProvider api { get; set; }
private readonly LocalUserStatisticsProvider statisticsProvider;
[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>();
/// <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]
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>
@ -64,35 +111,12 @@ namespace osu.Game.Beatmaps
return null;
}
private void populateValues()
protected override void Dispose(bool isDisposing)
{
if (api.LocalUser.Value.RulesetsStatistics == null)
return;
if (statisticsProvider.IsNotNull())
statisticsProvider.StatisticsUpdated -= onStatisticsUpdated;
foreach (var kvp in api.LocalUser.Value.RulesetsStatistics)
{
// 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);
}
base.Dispose(isDisposing);
}
}
}

View File

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

View File

@ -20,9 +20,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards
public abstract partial class BeatmapCard : OsuClickableContainer, IHasContextMenu
{
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; }

View File

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

View File

@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4, 0),
Spacing = new Vector2(3, 0),
Children = new Drawable[]
{
new BeatmapSetOnlineStatusPill
@ -33,13 +33,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards
AutoSizeAxes = Axes.Both,
Status = beatmapSet.Status,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
Origin = Anchor.CentreLeft,
TextSize = 13f
},
new DifficultySpectrumDisplay(beatmapSet)
{
Anchor = 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 DownloadInProgressContent => downloadProgressBar;
public const float HEIGHT = 100;
public const float HEIGHT = 80;
[Cached]
private readonly BeatmapCardContent content;
@ -69,7 +69,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Padding = new MarginPadding { Right = CORNER_RADIUS },
Child = leftIconArea = new FillFlowContainer
{
Margin = new MarginPadding(5),
Margin = new MarginPadding(4),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(1)
@ -81,7 +81,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Width = WIDTH - HEIGHT + CORNER_RADIUS,
FavouriteState = { BindTarget = FavouriteState },
ButtonsCollapsedWidth = CORNER_RADIUS,
ButtonsExpandedWidth = 30,
ButtonsExpandedWidth = 24,
Children = new Drawable[]
{
new FillFlowContainer
@ -110,7 +110,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
new TruncatingSpriteText
{
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,
},
titleBadgeArea = new FillFlowContainer
@ -143,7 +143,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
new TruncatingSpriteText
{
Text = createArtistText(),
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
},
Empty()
@ -153,11 +153,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
new LinkFlowContainer(s =>
{
s.Shadow = false;
s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold);
s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold);
}).With(d =>
{
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.AddUserLink(BeatmapSet.Author);
}),
@ -177,7 +177,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 3),
Spacing = new Vector2(0, 2),
AlwaysPresent = true,
Children = new Drawable[]
{
@ -186,7 +186,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Spacing = new Vector2(8, 0),
Alpha = 0,
AlwaysPresent = true,
ChildrenEnumerable = createStatistics()
@ -197,7 +197,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
downloadProgressBar = new BeatmapCardDownloadProgressBar
{
RelativeSizeAxes = Axes.X,
Height = 6,
Height = 5,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
State = { BindTarget = DownloadTracker.State },
@ -213,17 +213,17 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 10, Vertical = 13 },
Padding = new MarginPadding { Horizontal = 8, Vertical = 10 },
Child = new BeatmapCardDifficultyList(BeatmapSet)
};
c.Expanded.BindTarget = Expanded;
});
if (BeatmapSet.HasVideo)
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(20) });
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
if (BeatmapSet.HasStoryboard)
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) });
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
if (BeatmapSet.FeaturedInSpotlight)
{
@ -231,7 +231,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
Anchor = 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,
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,
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,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0),
Spacing = new Vector2(4, 0),
Children = new Drawable[]
{
spriteIcon = new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(10),
Size = new Vector2(8),
Margin = new MarginPadding { Top = 1 }
},
spriteText = new OsuSpriteText
{
Anchor = 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)
{
beatmapInfo.WidescreenStoryboard = false;
beatmapInfo.SamplesMatchPlaybackRate = false;
}
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.HitLighting, true);
SetDefault(OsuSetting.StarFountains, true);
SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always);
SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true);
@ -214,6 +215,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.EditorContractSidebars, false);
SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false);
SetDefault(OsuSetting.AlwaysRequireHoldingForPause, false);
}
protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup)
@ -413,6 +415,7 @@ namespace osu.Game.Configuration
NotifyOnPrivateMessage,
UIHoldActivationDelay,
HitLighting,
StarFountains,
MenuBackgroundSource,
GameplayDisableWinKey,
SeasonalBackgroundMode,
@ -444,5 +447,6 @@ namespace osu.Game.Configuration
EditorRotationOrigin,
EditorTimelineShowBreaks,
EditorAdjustExistingObjectsOnTimingChanges,
AlwaysRequireHoldingForPause
}
}

View File

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

View File

@ -74,6 +74,11 @@ namespace osu.Game.Localisation
/// </summary>
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>
/// "Always show key overlay"
/// </summary>
@ -89,6 +94,11 @@ namespace osu.Game.Localisation
/// </summary>
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>
/// "Always play first combo break sound"
/// </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");
/// <summary>
/// "{0}ms (speed {1})"
/// "{0}ms (speed {1:N1})"
/// </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>
/// "Touch control scheme"

View File

@ -59,7 +59,6 @@ namespace osu.Game.Online.API
public IBindable<APIUser> LocalUser => localUser;
public IBindableList<APIRelation> Friends => friends;
public IBindable<UserActivity> Activity => activity;
public IBindable<UserStatistics> Statistics => statistics;
public INotificationsClient NotificationsClient { get; }
@ -74,8 +73,6 @@ namespace osu.Game.Online.API
private Bindable<UserStatus?> configStatus { 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));
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
@ -604,14 +601,6 @@ namespace osu.Game.Online.API
flushQueue();
}
public void UpdateStatistics(UserStatistics newStatistics)
{
statistics.Value = newStatistics;
if (IsLoggedIn)
localUser.Value.Statistics = newStatistics;
}
public void UpdateLocalFriends()
{
if (!IsLoggedIn)
@ -630,11 +619,7 @@ namespace osu.Game.Online.API
private static APIUser createGuestUser() => new GuestUser();
private void setLocalUser(APIUser user) => Scheduler.Add(() =>
{
localUser.Value = user;
statistics.Value = user.Statistics;
}, false);
private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false);
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<UserStatistics?> Statistics { get; } = new Bindable<UserStatistics?>();
public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient();
INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient;
@ -178,11 +176,6 @@ namespace osu.Game.Online.API
private void onSuccessfulLogin()
{
state.Value = APIState.Online;
Statistics.Value = new UserStatistics
{
GlobalRank = 1,
CountryRank = 1
};
}
public void Logout()
@ -193,14 +186,6 @@ namespace osu.Game.Online.API
LocalUser.Value = new GuestUser();
}
public void UpdateStatistics(UserStatistics newStatistics)
{
Statistics.Value = newStatistics;
if (IsLoggedIn)
LocalUser.Value.Statistics = newStatistics;
}
public void UpdateLocalFriends()
{
}
@ -220,7 +205,6 @@ namespace osu.Game.Online.API
IBindable<APIUser> IAPIProvider.LocalUser => LocalUser;
IBindableList<APIRelation> IAPIProvider.Friends => Friends;
IBindable<UserActivity> IAPIProvider.Activity => Activity;
IBindable<UserStatistics?> IAPIProvider.Statistics => Statistics;
/// <summary>
/// Skip 2FA requirement for next login.

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ using osu.Game.Online.API;
namespace osu.Game.Online.Rooms
{
public class JoinRoomRequest : APIRequest
public class JoinRoomRequest : APIRequest<Room>
{
public readonly Room Room;
public readonly string? Password;

View File

@ -366,12 +366,8 @@ namespace osu.Game.Online.Rooms
{
RoomID = other.RoomID;
Name = other.Name;
Category = other.Category;
if (other.Host != null && Host?.Id != other.Host.Id)
Host = other.Host;
Host = other.Host;
ChannelId = other.ChannelId;
Status = other.Status;
Availability = other.Availability;
@ -388,22 +384,10 @@ namespace osu.Game.Online.Rooms
PlaylistItemStats = other.PlaylistItemStats;
CurrentPlaylistItem = other.CurrentPlaylistItem;
AutoSkip = other.AutoSkip;
other.RemoveExpiredPlaylistItems();
Playlist = other.Playlist;
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)]
public class RoomPlaylistItemStats
{

View File

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

View File

@ -2,18 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Game.Extensions;
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.Scoring;
using osu.Game.Users;
namespace osu.Game.Online
{
@ -22,8 +18,10 @@ namespace osu.Game.Online
/// </summary>
public partial class UserStatisticsWatcher : Component
{
public IBindable<UserStatisticsUpdate?> LatestUpdate => latestUpdate;
private readonly Bindable<UserStatisticsUpdate?> latestUpdate = new Bindable<UserStatisticsUpdate?>();
private readonly LocalUserStatisticsProvider statisticsProvider;
public IBindable<ScoreBasedUserStatisticsUpdate?> LatestUpdate => latestUpdate;
private readonly Bindable<ScoreBasedUserStatisticsUpdate?> latestUpdate = new Bindable<ScoreBasedUserStatisticsUpdate?>();
[Resolved]
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 Dictionary<string, UserStatistics>? latestStatistics;
public UserStatisticsWatcher(LocalUserStatisticsProvider statisticsProvider)
{
this.statisticsProvider = statisticsProvider;
}
protected override void LoadComplete()
{
base.LoadComplete();
api.LocalUser.BindValueChanged(user => onUserChanged(user.NewValue), true);
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)
{
if (userId != api.LocalUser.Value?.OnlineID)
@ -98,30 +69,11 @@ namespace osu.Game.Online
if (!watchedScores.Remove(scoreId, out var scoreInfo))
return;
requestStatisticsUpdate(userId, scoreInfo);
}
private void requestStatisticsUpdate(int userId, ScoreInfo scoreInfo)
{
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;
statisticsProvider.RefetchStatistics(scoreInfo.Ruleset, u => Schedule(() =>
{
if (u.OldStatistics != null)
latestUpdate.Value = new ScoreBasedUserStatisticsUpdate(scoreInfo, u.OldStatistics, u.NewStatistics);
}));
}
protected override void Dispose(bool isDisposing)

View File

@ -148,8 +148,7 @@ namespace osu.Game
[Resolved]
private FrameworkConfigManager frameworkConfig { get; set; }
[Cached]
private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender();
private DifficultyRecommender difficultyRecommender;
[Cached]
private readonly LegacyImportManager legacyImportManager = new LegacyImportManager();
@ -1069,7 +1068,11 @@ namespace osu.Game
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
{
OnHome = delegate
@ -1139,7 +1142,6 @@ namespace osu.Game
loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
loadComponentSingleFile(new DetachedBeatmapStore(), Add, true);
Add(difficultyRecommender);
Add(externalLinkOpener = new ExternalLinkOpener());
Add(new MusicKeyBindingHandler());
Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen));

View File

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

View File

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

View File

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

View File

@ -31,6 +31,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
LabelText = GraphicsSettingsStrings.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),
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.AlwaysRequireHoldForMenu,
Current = config.GetBindable<bool>(OsuSetting.AlwaysRequireHoldingForPause),
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.AlwaysShowHoldForMenuButton,
Current = config.GetBindable<bool>(OsuSetting.AlwaysShowHoldForMenuButton),

View File

@ -268,7 +268,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private void updateScreenModeWarning()
{
if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS)
// Can be removed once we stop supporting SDL2.
if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && !FrameworkEnvironment.UseSDL3)
{
if (windowModeDropdown.Current.Value == WindowMode.Fullscreen)
windowModeDropdown.SetNoticeText(LayoutSettingsStrings.FullscreenMacOSNote, true);

View File

@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Toolbar
{
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> pp = null!;
@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Toolbar
};
if (userStatisticsWatcher != null)
((IBindable<UserStatisticsUpdate?>)LatestUpdate).BindTo(userStatisticsWatcher.LatestUpdate);
((IBindable<ScoreBasedUserStatisticsUpdate?>)LatestUpdate).BindTo(userStatisticsWatcher.LatestUpdate);
}
protected override void LoadComplete()

View File

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

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Game.Rulesets.Objects;
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,
/// and <see langword="null"/> if unstable rate cannot be calculated due to <paramref name="hitEvents"/> being empty.
/// </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));
int count = 0;
double mean = 0;
double sumOfSquares = 0;
result ??= new UnstableRateCalculationResult();
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))
continue;
count++;
result.EventCount++;
// Division by gameplay rate is to account for TimeOffset scaling with gameplay rate.
double currentValue = e.TimeOffset / e.GameplayRate!.Value;
double nextMean = mean + (currentValue - mean) / count;
sumOfSquares += (currentValue - mean) * (currentValue - nextMean);
mean = nextMean;
double nextMean = result.Mean + (currentValue - result.Mean) / result.EventCount;
result.SumOfSquares += (currentValue - result.Mean) * (currentValue - nextMean);
result.Mean = nextMean;
}
if (count == 0)
if (result.EventCount == 0)
return null;
return 10.0 * Math.Sqrt(sumOfSquares / count);
return result;
}
/// <summary>
@ -65,6 +70,39 @@ namespace osu.Game.Rulesets.Scoring
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"/>.
/// No time values are provided (meaning instantaneous hit or miss).
/// </summary>
public static HitWindows Empty => new EmptyHitWindows();
public static HitWindows Empty { get; } = new EmptyHitWindows();
public HitWindows()
{
@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
protected virtual DifficultyRange[] GetRanges() => base_ranges;
public class EmptyHitWindows : HitWindows
private class EmptyHitWindows : HitWindows
{
private static readonly DifficultyRange[] ranges =
{

View File

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

View File

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

View File

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

View File

@ -8,6 +8,9 @@ using osu.Game.Graphics.Sprites;
using osuTK;
using osu.Game.Graphics;
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.Game.Beatmaps;
@ -27,26 +30,60 @@ namespace osu.Game.Screens.Menu
public SongTicker()
{
AutoSizeAxes = Axes.Both;
Child = new FillFlowContainer
InternalChildren = new Drawable[]
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 3),
Children = new Drawable[]
new Container
{
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,
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)
new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Radius = 75,
Type = EdgeEffectType.Shadow,
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)
{
incoming.RemoveExpiredPlaylistItems();
RoomManager.AddOrUpdateRoom(incoming);
}
initialRoomsReceived.Value = true;
tcs.SetResult(true);

View File

@ -72,9 +72,13 @@ namespace osu.Game.Screens.OnlinePlay.Components
currentJoinRoomRequest?.Cancel();
currentJoinRoomRequest = new JoinRoomRequest(room, password);
currentJoinRoomRequest.Success += () =>
currentJoinRoomRequest.Success += result =>
{
joinedRoom.Value = room;
AddOrUpdateRoom(result);
room.CopyFrom(result); // Also copy back to the source model, since this is likely to have been stored elsewhere.
onSuccess?.Invoke(room);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,41 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Scoring;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
/// <summary>
/// Shows a user's best score in a playlist item, with scores around included.
/// </summary>
public partial class PlaylistItemUserBestResultsScreen : PlaylistItemResultsScreen
{
private readonly int userId;
public PlaylistItemUserBestResultsScreen(long roomId, PlaylistItem playlistItem, int userId)
: base(null, roomId, playlistItem)
{
this.userId = userId;
}
protected override APIRequest<MultiplayerScore> CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId);
protected override ScoreInfo[] PerformSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null)
{
var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot);
Schedule(() =>
{
// Prefer selecting the local user's score, or otherwise default to the first visible score.
SelectedScore.Value ??= scoreInfos.FirstOrDefault(s => s.UserID == userId) ?? scoreInfos.FirstOrDefault();
});
return scoreInfos;
}
}
}

View File

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

View File

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

View File

@ -12,6 +12,7 @@ using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Graphics.Cursor;
using osu.Game.Input;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
@ -34,6 +35,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private readonly IBindable<bool> isIdle = new BindableBool();
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved(CanBeNull = true)]
private IdleTracker? idleTracker { get; set; }
@ -145,7 +149,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
RequestResults = item =>
{
Debug.Assert(Room.RoomID != null);
ParentScreen?.Push(new PlaylistItemUserResultsScreen(null, Room.RoomID.Value, item));
ParentScreen?.Push(new PlaylistItemUserBestResultsScreen(Room.RoomID.Value, item, api.LocalUser.Value.Id));
}
}
},

View File

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

View File

@ -28,6 +28,8 @@ namespace osu.Game.Screens.Play.HUD
private const float alpha_when_invalid = 0.3f;
private readonly Bindable<bool> valid = new Bindable<bool>();
private HitEventExtensions.UnstableRateCalculationResult? unstableRateResult;
[Resolved]
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));
}
private bool changesUnstableRate(JudgementResult judgement)
=> !(judgement.HitObject.HitWindows is HitWindows.EmptyHitWindows) && judgement.IsHit;
protected override void LoadComplete()
{
base.LoadComplete();
@ -56,13 +55,20 @@ namespace osu.Game.Screens.Play.HUD
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()
{
double? unstableRate = scoreProcessor.HitEvents.CalculateUnstableRate();
unstableRateResult = scoreProcessor.HitEvents.CalculateUnstableRate(unstableRateResult);
double? unstableRate = unstableRateResult?.Result;
valid.Value = unstableRate != null;
if (unstableRate != null)
Current.Value = (int)Math.Round(unstableRate.Value);
}

View File

@ -5,8 +5,10 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.Menu;
@ -18,9 +20,13 @@ namespace osu.Game.Screens.Play
private StarFountain leftFountain = null!;
private StarFountain rightFountain = null!;
private Bindable<bool> kiaiStarFountains = null!;
[BackgroundDependencyLoader]
private void load()
private void load(OsuConfigManager config)
{
kiaiStarFountains = config.GetBindable<bool>(OsuSetting.StarFountains);
RelativeSizeAxes = Axes.Both;
Children = new[]
@ -48,6 +54,9 @@ namespace osu.Game.Screens.Play
{
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
if (!kiaiStarFountains.Value)
return;
if (effectPoint.KiaiMode && !isTriggered)
{
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)
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.
//
@ -990,7 +992,7 @@ namespace osu.Game.Screens.Play
ScoreProcessor.FailScore(Score.ScoreInfo);
OnFail();
if (GameplayState.Mods.OfType<IApplicableFailOverride>().Any(m => m.RestartOnFail))
if (restartOnFail)
Restart(true);
});
}

View File

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

View File

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

View File

@ -58,7 +58,7 @@ namespace osu.Game.Screens.Ranking.Statistics
/// <param name="hitEvents">The <see cref="HitEvent"/>s to display the timing distribution of.</param>
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>>();
}

View File

@ -15,10 +15,10 @@ namespace osu.Game.Screens.Ranking.Statistics
/// Creates and computes an <see cref="UnstableRate"/> statistic.
/// </summary>
/// <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")
{
Value = hitEvents.CalculateUnstableRate();
Value = hitEvents.CalculateUnstableRate()?.Result;
}
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;
public Bindable<UserStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<UserStatisticsUpdate?>();
public Bindable<ScoreBasedUserStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<ScoreBasedUserStatisticsUpdate?>();
private LoadingLayer loadingLayer = null!;
private GridContainer content = null!;
@ -86,7 +86,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User
FinishTransforms(true);
}
private void onUpdateReceived(ValueChangedEvent<UserStatisticsUpdate?> update)
private void onUpdateReceived(ValueChangedEvent<ScoreBasedUserStatisticsUpdate?> update)
{
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 Bindable<UserStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<UserStatisticsUpdate?>();
public Bindable<ScoreBasedUserStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<ScoreBasedUserStatisticsUpdate?>();
private readonly Func<UserStatistics, T> accessor;
@ -113,7 +113,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User
StatisticsUpdate.BindValueChanged(onStatisticsUpdate, true);
}
private void onStatisticsUpdate(ValueChangedEvent<UserStatisticsUpdate?> statisticsUpdate)
private void onStatisticsUpdate(ValueChangedEvent<ScoreBasedUserStatisticsUpdate?> statisticsUpdate)
{
var update = statisticsUpdate.NewValue;

View File

@ -18,9 +18,9 @@ namespace osu.Game.Screens.Ranking.Statistics
{
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)
{

View File

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

View File

@ -71,7 +71,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
return true;
}
joinRoomRequest.TriggerSuccess();
joinRoomRequest.TriggerSuccess(createResponseRoom(room, true));
return true;
}

View File

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

View File

@ -41,6 +41,12 @@ namespace osu.Game.Users
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]
public class ChoosingBeatmap : UserActivity
{
@ -76,6 +82,7 @@ namespace osu.Game.Users
public override string GetStatus(bool hideIdentifiableInformation = false) => RulesetPlayingVerb;
public override string GetDetails(bool hideIdentifiableInformation = false) => BeatmapDisplayTitle;
public override int? GetBeatmapID(bool hideIdentifiableInformation = false) => BeatmapID;
}
[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.
? string.Empty
: 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]

View File

@ -4,13 +4,16 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
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.Overlays.Profile.Header.Components;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osuTK;
namespace osu.Game.Users
@ -24,13 +27,9 @@ namespace osu.Game.Users
private const int padding = 10;
private const int main_content_height = 80;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private ProfileValueDisplay globalRankDisplay = null!;
private ProfileValueDisplay countryRankDisplay = null!;
private readonly IBindable<UserStatistics?> statistics = new Bindable<UserStatistics?>();
private LoadingLayer loadingLayer = null!;
public UserRankPanel(APIUser user)
: base(user)
@ -43,13 +42,37 @@ namespace osu.Game.Users
private void load()
{
BorderColour = ColourProvider?.Light1 ?? Colours.GreyVioletLighter;
}
statistics.BindTo(api.Statistics);
statistics.BindValueChanged(stats =>
{
globalRankDisplay.Content = stats.NewValue?.GlobalRank?.ToLocalisableString("\\##,##0") ?? "-";
countryRankDisplay.Content = stats.NewValue?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-";
}, true);
[Resolved]
private LocalUserStatisticsProvider? statisticsProvider { get; set; }
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
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()
@ -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 void Dispose(bool isDisposing)
{
if (statisticsProvider.IsNotNull())
statisticsProvider.StatisticsUpdated -= onStatisticsUpdated;
base.Dispose(isDisposing);
}
}
}