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:
commit
2e6f43a75d
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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"),
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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));
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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 };
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
});
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -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).
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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 }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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>
|
||||
|
154
osu.Game/Localisation/MenuTipStrings.cs
Normal file
154
osu.Game/Localisation/MenuTipStrings.cs
Normal 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 "Screen Scaling" 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 "osu!direct" 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 "playlists" 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 "FPS" 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}";
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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();
|
||||
|
92
osu.Game/Online/LocalUserStatisticsProvider.cs
Normal file
92
osu.Game/Online/LocalUserStatisticsProvider.cs
Normal 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);
|
||||
}
|
@ -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;
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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;
|
@ -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)
|
||||
|
@ -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));
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -71,7 +71,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
|
||||
? new BeatmapCardNormal(model)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
@ -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)
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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);
|
||||
|
@ -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()
|
||||
|
@ -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))
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 =
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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)];
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -36,7 +36,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
|
||||
|
||||
req.Success += result =>
|
||||
{
|
||||
result.RemoveExpiredPlaylistItems();
|
||||
RoomManager.AddOrUpdateRoom(result);
|
||||
tcs.SetResult(true);
|
||||
};
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>>();
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
|
||||
return true;
|
||||
}
|
||||
|
||||
joinRoomRequest.TriggerSuccess();
|
||||
joinRoomRequest.TriggerSuccess(createResponseRoom(room, true));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user