1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-19 18:19:54 +08:00

Compare commits

...

40 Commits

44 changed files with 530 additions and 126 deletions
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Replays;
using osuTK;
@@ -17,5 +18,8 @@ namespace osu.Game.Rulesets.EmptyFreeform.Replays
if (button.HasValue)
Actions.Add(button.Value);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is EmptyFreeformReplayFrame freeformFrame && Time == freeformFrame.Time && Position == freeformFrame.Position && Actions.SequenceEqual(freeformFrame.Actions);
}
}
@@ -9,5 +9,8 @@ namespace osu.Game.Rulesets.Pippidon.Replays
public class PippidonReplayFrame : ReplayFrame
{
public Vector2 Position;
public override bool IsEquivalentTo(ReplayFrame other)
=> other is PippidonReplayFrame pippidonFrame && Time == pippidonFrame.Time && Position == pippidonFrame.Position;
}
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.EmptyScrolling.Replays
@@ -15,5 +16,8 @@ namespace osu.Game.Rulesets.EmptyScrolling.Replays
if (button.HasValue)
Actions.Add(button.Value);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is EmptyScrollingReplayFrame scrollingFrame && Time == scrollingFrame.Time && Actions.SequenceEqual(scrollingFrame.Actions);
}
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Pippidon.Replays
@@ -15,5 +16,8 @@ namespace osu.Game.Rulesets.Pippidon.Replays
if (button.HasValue)
Actions.Add(button.Value);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is PippidonReplayFrame pippidonFrame && Time == pippidonFrame.Time && Actions.SequenceEqual(pippidonFrame.Actions);
}
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays;
@@ -64,5 +65,12 @@ namespace osu.Game.Rulesets.Catch.Replays
return new LegacyReplayFrame(Time, Position, null, state);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is CatchReplayFrame catchFrame
&& Time == catchFrame.Time
&& Position == catchFrame.Position
&& Dashing == catchFrame.Dashing
&& Actions.SequenceEqual(catchFrame.Actions);
}
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays;
@@ -47,5 +48,8 @@ namespace osu.Game.Rulesets.Mania.Replays
return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is ManiaReplayFrame maniaFrame && Time == maniaFrame.Time && Actions.SequenceEqual(maniaFrame.Actions);
}
}
@@ -78,6 +78,16 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("smoke button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.Smoke])));
}
[Test]
public void TestPressAndReleaseOnSameFrame()
{
seekTo(0);
AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single()));
AddStep("press X", () => InputManager.PressKey(Key.X));
AddStep("release X", () => InputManager.ReleaseKey(Key.X));
AddAssert("right button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.RightButton])));
}
private void seekTo(double time)
{
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays;
@@ -47,5 +48,8 @@ namespace osu.Game.Rulesets.Osu.Replays
return new LegacyReplayFrame(Time, Position.X, Position.Y, state);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is OsuReplayFrame osuFrame && Time == osuFrame.Time && Position == osuFrame.Position && Actions.SequenceEqual(osuFrame.Actions);
}
}
@@ -148,5 +148,96 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 8 && Player.ScoreProcessor.Accuracy.Value == 1
});
/// <summary>
/// Regression tests a case of 1/3rd conversion where there are exactly div-3 number of hitobjects.
/// </summary>
[Test]
public void TestOnlyOneThirdConversion()
{
CreateModTest(new ModTestData
{
Mod = new TaikoModSimplifiedRhythm
{
OneThirdConversion = { Value = true },
},
Autoplay = false,
CreateBeatmap = () => new Beatmap
{
HitObjects = new List<HitObject>
{
new Hit { StartTime = 1000, Type = HitType.Centre },
new Hit { StartTime = 1333, Type = HitType.Centre }, // mod removes this
new Hit { StartTime = 1666, Type = HitType.Centre }, // mod moves this to 1500
new Hit { StartTime = 2000, Type = HitType.Centre },
new Hit { StartTime = 2333, Type = HitType.Centre }, // mod removes this
new Hit { StartTime = 2666, Type = HitType.Centre }, // mod moves this to 2500
},
},
ReplayFrames = new List<ReplayFrame>
{
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1200),
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
new TaikoReplayFrame(1700),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2200),
new TaikoReplayFrame(2500, TaikoAction.LeftCentre),
new TaikoReplayFrame(2700),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 4 && Player.ScoreProcessor.Accuracy.Value == 1
});
}
/// <summary>
/// Regression tests a case of 1/6th conversion where there are exactly div-6 number of hitobjects.
/// </summary>
[Test]
public void TestOnlyOneSixthConversion() => CreateModTest(new ModTestData
{
Mod = new TaikoModSimplifiedRhythm
{
OneSixthConversion = { Value = true }
},
Autoplay = false,
CreateBeatmap = () => new Beatmap
{
HitObjects = new List<HitObject>
{
new Hit { StartTime = 1000, Type = HitType.Centre },
new Hit { StartTime = 1166, Type = HitType.Centre }, // mod removes this
new Hit { StartTime = 1333, Type = HitType.Centre }, // mod moves this to 1250
new Hit { StartTime = 1500, Type = HitType.Centre },
new Hit { StartTime = 1666, Type = HitType.Centre }, // mod removes this
new Hit { StartTime = 1833, Type = HitType.Centre }, // mod moves this to 1750
new Hit { StartTime = 2000, Type = HitType.Centre },
new Hit { StartTime = 2166, Type = HitType.Centre }, // mod removes this
new Hit { StartTime = 2333, Type = HitType.Centre }, // mod moves this to 2250
new Hit { StartTime = 2500, Type = HitType.Centre },
new Hit { StartTime = 2666, Type = HitType.Centre }, // mod removes this
new Hit { StartTime = 2833, Type = HitType.Centre }, // mod moves this to 2750
},
},
ReplayFrames = new List<ReplayFrame>
{
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1200),
new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
new TaikoReplayFrame(1450),
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
new TaikoReplayFrame(1600),
new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
new TaikoReplayFrame(1800),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2200),
new TaikoReplayFrame(2250, TaikoAction.LeftCentre),
new TaikoReplayFrame(2450),
new TaikoReplayFrame(2500, TaikoAction.LeftCentre),
new TaikoReplayFrame(2600),
new TaikoReplayFrame(2750, TaikoAction.LeftCentre),
new TaikoReplayFrame(2800),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 8 && Player.ScoreProcessor.Accuracy.Value == 1
});
}
}
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
var taikoBeatmap = (TaikoBeatmap)beatmap;
var controlPointInfo = taikoBeatmap.ControlPointInfo;
Hit[] hits = taikoBeatmap.HitObjects.Where(obj => obj is Hit).Cast<Hit>().ToArray();
Hit[] hits = taikoBeatmap.HitObjects.OfType<Hit>().ToArray();
if (hits.Length == 0)
return;
@@ -61,10 +61,10 @@ namespace osu.Game.Rulesets.Taiko.Mods
if (inPattern)
{
// pattern continues
if (snapValue == baseRhythm) continue;
if (snapValue == baseRhythm)
continue;
inPattern = false;
processPattern(i);
}
else
@@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
if (indexInPattern % 3 == 1)
taikoBeatmap.HitObjects.Remove(hits[j]);
else if (indexInPattern % 3 == 2)
hits[j].StartTime = hits[j + 1].StartTime - controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / adjustedRhythm;
hits[j].StartTime = hits[j - 2].StartTime + controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / adjustedRhythm;
break;
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays;
@@ -42,5 +43,8 @@ namespace osu.Game.Rulesets.Taiko.Replays
return new LegacyReplayFrame(Time, null, null, state);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is TaikoReplayFrame taikoFrame && Time == taikoFrame.Time && Actions.SequenceEqual(taikoFrame.Actions);
}
}
+37 -4
View File
@@ -259,9 +259,6 @@ namespace osu.Game.Tests.Mods
new MultiplayerTestScenario(true, true, [new OsuModPerfect()], []),
new MultiplayerTestScenario(true, true, [new OsuModDoubleTime()], []),
new MultiplayerTestScenario(true, true, [new OsuModNightcore()], []),
new MultiplayerTestScenario(true, true, [new OsuModHidden()], []),
new MultiplayerTestScenario(true, true, [new OsuModFlashlight()], []),
new MultiplayerTestScenario(true, true, [new OsuModAccuracyChallenge()], []),
new MultiplayerTestScenario(true, true, [new OsuModDifficultyAdjust()], []),
new MultiplayerTestScenario(true, true, [new ModWindUp()], []),
new MultiplayerTestScenario(true, true, [new ModWindDown()], []),
@@ -347,8 +344,44 @@ namespace osu.Game.Tests.Mods
{
if (mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && !commonAcronyms.Contains(mod.Acronym))
Assert.Fail($"{mod.GetType().ReadableName()} declares {nameof(Mod.ValidForFreestyleAsRequiredMod)} but does not exist in all four basic rulesets!");
// downgraded to warning, because there are valid reasons why they may still not be specified to be valid for freestyle as required
// (see `TestModsValidForRequiredFreestyleAreConsistentlyCompatibleAcrossRulesets()` test case below).
if (!mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && commonAcronyms.Contains(mod.Acronym))
Assert.Fail($"{mod.GetType().ReadableName()} does not declare {nameof(Mod.ValidForFreestyleAsRequiredMod)} but exists in all four basic rulesets!");
Assert.Warn($"{mod.GetType().ReadableName()} does not declare {nameof(Mod.ValidForFreestyleAsRequiredMod)} but exists in all four basic rulesets.");
}
}
});
}
[Test]
public void TestModsValidForRequiredFreestyleAreConsistentlyCompatibleAcrossRulesets()
{
Dictionary<(string firstMod, string secondMod), bool> compatibilityMap = new Dictionary<(string, string), bool>();
Assert.Multiple(() =>
{
for (int rulesetId = 0; rulesetId < 4; ++rulesetId)
{
var rulesetStore = new AssemblyRulesetStore();
var ruleset = rulesetStore.GetRuleset(rulesetId)!.CreateInstance();
var modsValidForFreestyleAsRequired = ruleset.CreateAllMods().Where(m => m.ValidForFreestyleAsRequiredMod).OrderBy(m => m.Acronym).ToList();
for (int i = 0; i < modsValidForFreestyleAsRequired.Count; i++)
{
for (int j = i; j < modsValidForFreestyleAsRequired.Count; ++j)
{
var first = modsValidForFreestyleAsRequired[i];
var second = modsValidForFreestyleAsRequired[j];
bool compatible = ModUtils.CheckCompatibleSet([first, second]);
if (!compatibilityMap.TryGetValue((first.Acronym, second.Acronym), out bool previousCompatible))
compatibilityMap[(first.Acronym, second.Acronym)] = compatible;
else if (previousCompatible != compatible)
Assert.Fail($"{first.Acronym} and {second.Acronym} declare {nameof(Mod.ValidForFreestyleAsRequiredMod)} while not being consistently compatible in all four rulesets!");
}
}
}
});
@@ -383,6 +383,9 @@ namespace osu.Game.Tests.NonVisual
IsImportant = isImportant;
FrameIndex = frameIndex;
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is TestReplayFrame testFrame && Time == testFrame.Time && IsImportant == testFrame.IsImportant && FrameIndex == testFrame.FrameIndex;
}
private class TestInputHandler : FramedReplayInputHandler<TestReplayFrame>
@@ -136,7 +136,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
IBindableList<GameplayLeaderboardScore> IGameplayLeaderboardProvider.Scores => Scores;
public BindableList<GameplayLeaderboardScore> Scores { get; } = new BindableList<GameplayLeaderboardScore>();
public bool IsPartial { get; } = false;
public TestGameplayLeaderboardProvider()
{
@@ -147,8 +146,8 @@ namespace osu.Game.Tests.Visual.Gameplay
User = new APIUser { Username = $"User {i}" },
TotalScore = (20 - i) * 50_000,
Accuracy = i * 0.05,
Combo = i * 50
}, i == 19));
MaxCombo = i * 50,
}, i == 19, GameplayLeaderboardScore.ComboDisplayMode.Highest));
}
}
}
@@ -317,6 +317,9 @@ namespace osu.Game.Tests.Visual.Gameplay
Position = position;
Actions.AddRange(actions);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is TestReplayFrame testFrame && Time == testFrame.Time && Position == testFrame.Position && Actions.SequenceEqual(testFrame.Actions);
}
public enum TestAction
@@ -353,6 +353,9 @@ namespace osu.Game.Tests.Visual.Gameplay
return new LegacyReplayFrame(Time, Position.X, Position.Y, state);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is TestReplayFrame testFrame && Time == testFrame.Time && Position == testFrame.Position && Actions.SequenceEqual(testFrame.Actions);
}
public enum TestAction
@@ -66,10 +66,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2
foreach (var scoreInfo in getTestScores())
{
BeatmapLeaderboardScore.HighlightType? highlightType = null;
switch (scoreInfo.User.Id)
{
case 2:
highlightType = BeatmapLeaderboardScore.HighlightType.Own;
break;
case 1541390:
highlightType = BeatmapLeaderboardScore.HighlightType.Friend;
break;
}
fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo)
{
Rank = scoreInfo.Position,
IsPersonalBest = scoreInfo.User.Id == 2,
Highlight = highlightType,
Shear = Vector2.Zero,
});
}
@@ -104,10 +117,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2
foreach (var scoreInfo in getTestScores())
{
BeatmapLeaderboardScore.HighlightType? highlightType = null;
switch (scoreInfo.User.Id)
{
case 2:
highlightType = BeatmapLeaderboardScore.HighlightType.Own;
break;
case 1541390:
highlightType = BeatmapLeaderboardScore.HighlightType.Friend;
break;
}
fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo, sheared: false)
{
Rank = scoreInfo.Position,
IsPersonalBest = scoreInfo.User.Id == 2,
Highlight = highlightType,
});
}
@@ -205,7 +231,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
Position = 999,
Rank = ScoreRank.X,
Accuracy = 1,
MaxCombo = 244,
MaxCombo = 3000,
TotalScore = RNG.Next(1_800_000, 2_000_000),
MaximumStatistics = { { HitResult.Great, 3000 } },
Ruleset = new OsuRuleset().RulesetInfo,
@@ -223,7 +249,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
Position = 22333,
Rank = ScoreRank.S,
Accuracy = 0.1f,
MaxCombo = 32040,
MaxCombo = 2204,
TotalScore = RNG.Next(1_200_000, 1_500_000),
MaximumStatistics = { { HitResult.Great, 3000 } },
Ruleset = new OsuRuleset().RulesetInfo,
+2 -2
View File
@@ -108,7 +108,7 @@ namespace osu.Game.Graphics.Carousel
get => currentSelection.Model;
set
{
if (currentSelection.Model != value)
if (!CheckModelEquality(currentSelection.Model, value))
{
HandleItemSelected(value);
@@ -210,7 +210,7 @@ namespace osu.Game.Graphics.Carousel
/// <summary>
/// Check whether two models are the same for display purposes.
/// </summary>
protected virtual bool CheckModelEquality(object x, object y) => ReferenceEquals(x, y);
protected virtual bool CheckModelEquality(object? x, object? y) => ReferenceEquals(x, y);
/// <summary>
/// Create a drawable for the given carousel item so it can be displayed.
@@ -15,13 +15,15 @@ namespace osu.Game.Graphics.UserInterface
/// </summary>
public partial class LoadingSpinner : VisibilityContainer
{
public const float TRANSITION_DURATION = 500;
private readonly SpriteIcon spinner;
protected override bool StartHidden => true;
protected Container MainContents;
protected Drawable MainContents;
public const float TRANSITION_DURATION = 500;
private readonly Container? roundedContent;
private const float spin_duration = 900;
@@ -37,32 +39,46 @@ namespace osu.Game.Graphics.UserInterface
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Child = MainContents = new Container
if (withBox)
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 20,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
Child = MainContents = roundedContent = new Container
{
new Box
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 20,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
Colour = inverted ? Color4.White : Color4.Black,
RelativeSizeAxes = Axes.Both,
Alpha = withBox ? 0.7f : 0
},
spinner = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = inverted ? Color4.Black : Color4.White,
Scale = new Vector2(withBox ? 0.6f : 1),
RelativeSizeAxes = Axes.Both,
Icon = FontAwesome.Solid.CircleNotch
new Box
{
Colour = inverted ? Color4.White : Color4.Black,
RelativeSizeAxes = Axes.Both,
Alpha = 0.7f,
},
spinner = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = inverted ? Color4.Black : Color4.White,
Scale = new Vector2(0.6f),
RelativeSizeAxes = Axes.Both,
Icon = FontAwesome.Solid.CircleNotch
}
}
}
};
};
}
else
{
Child = MainContents = spinner = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = inverted ? Color4.Black : Color4.White,
RelativeSizeAxes = Axes.Both,
Icon = FontAwesome.Solid.CircleNotch
};
}
}
protected override void LoadComplete()
@@ -76,7 +92,8 @@ namespace osu.Game.Graphics.UserInterface
{
base.Update();
MainContents.CornerRadius = MainContents.DrawWidth / 4;
if (roundedContent != null)
roundedContent.CornerRadius = MainContents.DrawWidth / 4;
}
protected override void PopIn()
@@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class DefaultRankDisplayStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.DefaultRankDisplay";
/// <summary>
/// "Play samples on rank change"
/// </summary>
public static LocalisableString PlaySamplesOnRankChange => new TranslatableString(getKey(@"play_samples_on_rank_change"), @"Play samples on rank change");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
+2 -4
View File
@@ -247,12 +247,10 @@ namespace osu.Game.Online.Spectator
var convertedFrame = convertible.ToLegacy(currentBeatmap);
// only keep the last recorded frame for a given timestamp.
// this reduces redundancy of frames in the resulting replay.
//
// this is also done at `ReplayRecorded`, but needs to be done here as well
// it is also done at `ReplayRecorder`, but needs to be done here as well
// due to the flow being handled differently.
if (pendingFrames.LastOrDefault()?.Time == convertedFrame.Time)
if (pendingFrames.LastOrDefault()?.IsEquivalentTo(convertedFrame) == true)
pendingFrames[^1] = convertedFrame;
else
pendingFrames.Add(convertedFrame);
@@ -39,6 +39,11 @@ namespace osu.Game.Online.Spectator
/// </summary>
public readonly BindableInt Combo = new BindableInt();
/// <summary>
/// The highest combo achieved in the score thus far.
/// </summary>
public readonly BindableInt HighestCombo = new BindableInt();
/// <summary>
/// The <see cref="ScoringMode"/> used to calculate scores.
/// </summary>
@@ -157,6 +162,7 @@ namespace osu.Game.Online.Spectator
Accuracy.Value = frame.Header.Accuracy;
Combo.Value = frame.Header.Combo;
HighestCombo.Value = frame.Header.MaxCombo;
TotalScore.Value = frame.Header.TotalScore;
}
@@ -219,7 +219,7 @@ namespace osu.Game.Overlays.SkinEditor
}
}
public partial class DependencyBorrowingContainer : Container
private partial class DependencyBorrowingContainer : Container
{
protected override bool ShouldBeConsideredForInput(Drawable child) => false;
@@ -232,8 +232,19 @@ namespace osu.Game.Overlays.SkinEditor
this.donor = donor;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
new DependencyContainer(donor?.Dependencies ?? base.CreateChildDependencies(parent));
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var baseDependencies = base.CreateChildDependencies(parent);
if (donor == null)
return baseDependencies;
var dependencies = new DependencyContainer(donor.Dependencies);
// inject `SkinEditor` again *on top* of the borrowed dependencies.
// this is designed to let components know when they are being displayed in the context of the skin editor
// via attempting to resolve `SkinEditor`.
dependencies.CacheAs(baseDependencies.Get<SkinEditor>());
return dependencies;
}
}
}
}
@@ -64,5 +64,12 @@ namespace osu.Game.Replays.Legacy
{
return $"{Time}\t({MouseX},{MouseY})\t{MouseLeft}\t{MouseRight}\t{MouseLeft1}\t{MouseRight1}\t{MouseLeft2}\t{MouseRight2}\t{ButtonState}";
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is LegacyReplayFrame legacyFrame
&& Time == legacyFrame.Time
&& MouseX == legacyFrame.MouseX
&& MouseY == legacyFrame.MouseY
&& ButtonState == legacyFrame.ButtonState;
}
}
@@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.Mods
public override bool Ranked => true;
public override bool ValidForFreestyleAsRequiredMod => true;
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
{
get
-1
View File
@@ -37,7 +37,6 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => "Restricted view area.";
public override bool Ranked => UsesDefaultConfiguration;
public override bool ValidForFreestyleAsRequiredMod => true;
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public abstract BindableFloat SizeMultiplier { get; }
-1
View File
@@ -15,7 +15,6 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => OsuIcon.ModHidden;
public override ModType Type => ModType.DifficultyIncrease;
public override bool Ranked => UsesDefaultConfiguration;
public override bool ValidForFreestyleAsRequiredMod => true;
public virtual void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
{
+5
View File
@@ -30,5 +30,10 @@ namespace osu.Game.Rulesets.Replays
{
Time = time;
}
/// <summary>
/// Whether this frame is equivalent to <paramref name="other"/> with respect to replay recording.
/// </summary>
public virtual bool IsEquivalentTo(ReplayFrame other) => Time == other.Time;
}
}
+1 -2
View File
@@ -86,9 +86,8 @@ namespace osu.Game.Rulesets.UI
if (frame != null)
{
// only keep the last recorded frame for a given timestamp.
// this reduces redundancy of frames in the resulting replay.
if (last?.Time == frame.Time)
if (last?.IsEquivalentTo(frame) == true)
target.Replay.Frames[^1] = frame;
else
target.Replay.Frames.Add(frame);
+10
View File
@@ -20,6 +20,7 @@ using osu.Game.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges;
using osu.Framework.Localisation;
using osu.Game.Beatmaps.ControlPoints;
@@ -257,6 +258,15 @@ namespace osu.Game.Screens.Menu
protected override void OnMouseUp(MouseUpEvent e)
{
// HORRIBLE HACK
// This is here so that on mobile, the main menu button that progresses to song select can correctly progress to song select v2 when held.
// Once the temporary solution of holding the button to access song select v2 is removed, this should be too.
// Without this, the long-press-to-right-click flow intercepts the hold and converts it to a right click which would not trigger the button
// and therefore not progress to song select.
if (e.Button == MouseButton.Right && e.CurrentState.Mouse.LastSource is ISourcedFromTouch)
trigger(e);
// END OF HORRIBLE HACK
boxHoverLayer.FadeTo(0, 1000, Easing.OutQuint);
base.OnMouseUp(e);
}
+16 -2
View File
@@ -16,6 +16,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Backgrounds;
@@ -391,12 +392,27 @@ namespace osu.Game.Screens.Menu
protected override void OnMouseUp(MouseUpEvent e)
{
// HORRIBLE HACK
// This is here so that on mobile, the logo can correctly progress from main menu to song select v2 when held.
// Once the temporary solution of holding the logo to access song select v2 is removed, this should be too.
// Without this, the long-press-to-right-click flow intercepts the hold and converts it to a right click which would not trigger the logo
// and therefore not progress to song select.
if (e.Button == MouseButton.Right && e.CurrentState.Mouse.LastSource is ISourcedFromTouch)
triggerClick();
// END OF HORRIBLE HACK
if (e.Button != MouseButton.Left) return;
logoBounceContainer.ScaleTo(1f, 500, Easing.OutElastic);
}
protected override bool OnClick(ClickEvent e)
{
triggerClick();
return true;
}
private void triggerClick()
{
flashLayer.ClearTransforms();
flashLayer.Alpha = 0.4f;
@@ -408,8 +424,6 @@ namespace osu.Game.Screens.Menu
sampleClickChannel = sampleClick.GetChannel();
sampleClickChannel.Play();
}
return true;
}
protected override bool OnHover(HoverEvent e)
@@ -158,13 +158,23 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
}
else
{
LoadComponentsAsync(best.Select((s, index) => new BeatmapLeaderboardScore(s, sheared: false)
LoadComponentsAsync(best.Select((s, index) =>
{
Rank = index + 1,
IsPersonalBest = s.UserID == api.LocalUser.Value.Id,
Action = () => PresentScore?.Invoke(s.OnlineID),
SelectedMods = { BindTarget = SelectedMods },
IsValidMod = IsValidMod,
BeatmapLeaderboardScore.HighlightType? highlightType = null;
if (s.UserID == api.LocalUser.Value.Id)
highlightType = BeatmapLeaderboardScore.HighlightType.Own;
else if (api.Friends.Any(r => r.TargetID == s.UserID))
highlightType = BeatmapLeaderboardScore.HighlightType.Friend;
return new BeatmapLeaderboardScore(s, sheared: false)
{
Rank = index + 1,
Highlight = highlightType,
Action = () => PresentScore?.Invoke(s.OnlineID),
SelectedMods = { BindTarget = SelectedMods },
IsValidMod = IsValidMod,
};
}), loaded =>
{
scoreFlow.Clear();
@@ -181,7 +191,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
userBestContainer.Add(new BeatmapLeaderboardScore(userBest, sheared: false)
{
Rank = userBest.Position,
IsPersonalBest = true,
Highlight = BeatmapLeaderboardScore.HighlightType.Own,
Action = () => PresentScore?.Invoke(userBest.OnlineID),
SelectedMods = { BindTarget = SelectedMods },
IsValidMod = IsValidMod,
@@ -62,6 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
private UserCoverBackground userCover = null!;
private UpdateableAvatar userAvatar = null!;
private UpdateableFlag userFlag = null!;
private OsuSpriteText username = null!;
private Container teamFlagContainer = null!;
private OsuSpriteText userRankText = null!;
@@ -140,7 +141,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
},
new UpdateableFlag
userFlag = new UpdateableFlag
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@@ -241,6 +242,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
userCover.User = user;
userAvatar.User = user;
userFlag.CountryCode = user?.CountryCode ?? default;
teamFlagContainer.Child = new UpdateableTeamFlag(user?.Team)
{
Size = new Vector2(40, 20)
@@ -6,11 +6,14 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osuTK;
using osu.Game.Localisation;
namespace osu.Game.Screens.Play.HUD
{
@@ -19,6 +22,9 @@ namespace osu.Game.Screens.Play.HUD
[Resolved]
private ScoreProcessor scoreProcessor { get; set; } = null!;
[SettingSource(typeof(DefaultRankDisplayStrings), nameof(DefaultRankDisplayStrings.PlaySamplesOnRankChange))]
public BindableBool PlaySamples { get; set; } = new BindableBool(true);
public bool UsesFixedAnchor { get; set; }
private UpdateableRank rankDisplay = null!;
@@ -34,7 +40,7 @@ namespace osu.Game.Screens.Play.HUD
}
[BackgroundDependencyLoader]
private void load()
private void load(SkinEditor? skinEditor)
{
InternalChildren = new Drawable[]
{
@@ -45,6 +51,9 @@ namespace osu.Game.Screens.Play.HUD
RelativeSizeAxes = Axes.Both
},
};
if (skinEditor != null)
PlaySamples.Value = false;
}
protected override void LoadComplete()
@@ -55,7 +64,7 @@ namespace osu.Game.Screens.Play.HUD
rank.BindValueChanged(r =>
{
// Don't play rank-down sfx on quit/retry
if (r.NewValue != r.OldValue && r.NewValue > ScoreRank.F)
if (r.NewValue != r.OldValue && r.NewValue > ScoreRank.F && PlaySamples.Value)
{
if (r.NewValue > rankDisplay.Rank)
rankUpSample.Play();
@@ -8,6 +8,7 @@ using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Screens.Play;
using osu.Game.Users;
namespace osu.Game.Screens.Select.Leaderboards
@@ -40,7 +41,8 @@ namespace osu.Game.Screens.Select.Leaderboards
public BindableDouble Accuracy { get; } = new BindableDouble();
/// <summary>
/// The current combo of the score.
/// The combo of the score to display.
/// Can be either highest combo or current combo, depending on constructor parameters.
/// </summary>
public BindableInt Combo { get; } = new BindableInt();
@@ -87,33 +89,35 @@ namespace osu.Game.Screens.Select.Leaderboards
/// </summary>
public Bindable<long> DisplayOrder { get; } = new BindableLong();
public GameplayLeaderboardScore(IUser user, ScoreProcessor scoreProcessor, bool tracked)
public GameplayLeaderboardScore(GameplayState gameplayState, bool tracked, ComboDisplayMode comboMode)
{
User = gameplayState.Score.ScoreInfo.User;
Tracked = tracked;
var scoreProcessor = gameplayState.ScoreProcessor;
TotalScore.BindTarget = scoreProcessor.TotalScore;
Accuracy.BindTarget = scoreProcessor.Accuracy;
Combo.BindTarget = comboMode == ComboDisplayMode.Current ? scoreProcessor.Combo : scoreProcessor.HighestCombo;
GetDisplayScore = scoreProcessor.GetDisplayScore;
}
public GameplayLeaderboardScore(IUser user, SpectatorScoreProcessor scoreProcessor, bool tracked, ComboDisplayMode comboMode)
{
User = user;
Tracked = tracked;
TotalScore.BindTarget = scoreProcessor.TotalScore;
Accuracy.BindTarget = scoreProcessor.Accuracy;
Combo.BindTarget = scoreProcessor.Combo;
Combo.BindTarget = comboMode == ComboDisplayMode.Current ? scoreProcessor.Combo : scoreProcessor.HighestCombo;
GetDisplayScore = scoreProcessor.GetDisplayScore;
}
public GameplayLeaderboardScore(IUser user, SpectatorScoreProcessor scoreProcessor, bool tracked)
{
User = user;
Tracked = tracked;
TotalScore.BindTarget = scoreProcessor.TotalScore;
Accuracy.BindTarget = scoreProcessor.Accuracy;
Combo.BindTarget = scoreProcessor.Combo;
GetDisplayScore = scoreProcessor.GetDisplayScore;
}
public GameplayLeaderboardScore(ScoreInfo scoreInfo, bool tracked)
public GameplayLeaderboardScore(ScoreInfo scoreInfo, bool tracked, ComboDisplayMode comboMode)
{
User = scoreInfo.User;
Tracked = tracked;
TotalScore.Value = scoreInfo.TotalScore;
Accuracy.Value = scoreInfo.Accuracy;
Combo.Value = scoreInfo.MaxCombo;
Combo.Value = comboMode == ComboDisplayMode.Current ? scoreInfo.Combo : scoreInfo.MaxCombo;
TotalScoreTiebreaker = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds();
GetDisplayScore = scoreInfo.GetDisplayScore;
InitialPosition = scoreInfo.Position;
@@ -129,5 +133,11 @@ namespace osu.Game.Screens.Select.Leaderboards
TotalScore.BindTarget = displayScore;
GetDisplayScore = _ => displayScore.Value;
}
public enum ComboDisplayMode
{
Current,
Highest,
}
}
}
@@ -99,7 +99,11 @@ namespace osu.Game.Screens.Select.Leaderboards
var trackedUser = UserScores[user.Id];
var leaderboardScore = new GameplayLeaderboardScore(user, trackedUser.ScoreProcessor, user.Id == api.LocalUser.Value.Id)
var leaderboardScore = new GameplayLeaderboardScore(
user,
trackedUser.ScoreProcessor,
user.Id == api.LocalUser.Value.Id,
GameplayLeaderboardScore.ComboDisplayMode.Current)
{
HasQuit = { BindTarget = trackedUser.UserQuit },
TeamColour = UserScores[user.OnlineID].Team is int team ? getTeamColour(team) : null,
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -34,23 +35,29 @@ namespace osu.Game.Screens.Select.Leaderboards
isPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50;
List<GameplayLeaderboardScore> newScores = new List<GameplayLeaderboardScore>();
if (globalScores != null)
{
foreach (var topScore in globalScores.AllScores.OrderByTotalScore())
scores.Add(new GameplayLeaderboardScore(topScore, false));
{
newScores.Add(new GameplayLeaderboardScore(topScore, false, GameplayLeaderboardScore.ComboDisplayMode.Highest));
}
}
if (gameplayState != null)
{
var localScore = new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true)
var localScore = new GameplayLeaderboardScore(gameplayState, tracked: true, GameplayLeaderboardScore.ComboDisplayMode.Highest)
{
// Local score should always show lower than any existing scores in cases of ties.
TotalScoreTiebreaker = long.MaxValue
};
localScore.TotalScore.BindValueChanged(_ => sorting.Invalidate());
scores.Add(localScore);
newScores.Add(localScore);
}
scores.AddRange(newScores);
Scheduler.AddDelayed(sort, 1000, true);
}
+10 -3
View File
@@ -119,8 +119,15 @@ namespace osu.Game.Screens.SelectV2
#region Beatmap source hookup
private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed)
private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) => Schedule(() =>
{
// This callback is scheduled to ensure there's no added overhead during gameplay.
// If this ever becomes an issue, it's important to note that the actual carousel filtering is already
// implemented in a way it will only run when at song select.
//
// The overhead we are avoiding here is that of this method directly things like Items.IndexOf calls
// that can be slow for very large beatmap libraries. There are definitely ways to optimise this further.
// TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider.
// right now we are managing this locally which is a bit of added overhead.
IEnumerable<BeatmapSetInfo>? newItems = changed.NewItems?.Cast<BeatmapSetInfo>();
@@ -191,7 +198,7 @@ namespace osu.Game.Screens.SelectV2
Items.Clear();
break;
}
}
});
#endregion
@@ -553,7 +560,7 @@ namespace osu.Game.Screens.SelectV2
AddInternal(setPanelPool);
}
protected override bool CheckModelEquality(object x, object y)
protected override bool CheckModelEquality(object? x, object? y)
{
// In the confines of the carousel logic, we assume that CurrentSelection (and all items) are using non-stale
// BeatmapInfo reference, and that we can match based on beatmap / beatmapset (GU)IDs.
@@ -56,11 +56,14 @@ namespace osu.Game.Screens.SelectV2
public Func<Mod, bool> IsValidMod { get; set; } = _ => true;
public int? Rank { get; init; }
public bool IsPersonalBest { get; init; }
public HighlightType? Highlight { get; init; }
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
@@ -93,8 +96,6 @@ namespace osu.Game.Screens.SelectV2
private Colour4 backgroundColour;
private ColourInfo totalScoreBackgroundGradient;
private ColourInfo personalBestGradient;
private IBindable<ScoringMode> scoringMode { get; set; } = null!;
private Box background = null!;
@@ -109,7 +110,7 @@ namespace osu.Game.Screens.SelectV2
private Box totalScoreBackground = null!;
private FillFlowContainer statisticsContainer = null!;
private Container personalBestIndicator = null!;
private Container highlightGradient = null!;
private Container rankLabelStandalone = null!;
private Container rankLabelOverlay = null!;
@@ -142,7 +143,6 @@ namespace osu.Game.Screens.SelectV2
foregroundColour = colourProvider.Background5;
backgroundColour = colourProvider.Background3;
totalScoreBackgroundGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), backgroundColour);
personalBestGradient = ColourInfo.GradientHorizontal(personal_best_gradient_left, personal_best_gradient_right);
Child = new Container
{
@@ -176,15 +176,15 @@ namespace osu.Game.Screens.SelectV2
RelativeSizeAxes = Axes.Y,
Children = new Drawable[]
{
personalBestIndicator = new Container
highlightGradient = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = -10f },
Alpha = IsPersonalBest ? 1 : 0,
Colour = personalBestGradient,
Alpha = Highlight != null ? 1 : 0,
Colour = getHighlightColour(Highlight),
Child = new Box { RelativeSizeAxes = Axes.Both },
},
new RankLabel(Rank, sheared, darkText: IsPersonalBest)
new RankLabel(Rank, sheared, darkText: Highlight == HighlightType.Own)
{
RelativeSizeAxes = Axes.Both,
}
@@ -330,7 +330,11 @@ namespace osu.Game.Screens.SelectV2
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = getStatistics(score).Select(s => new ScoreComponentLabel(s, score)).ToList(),
Children = new Drawable[]
{
new ScoreComponentLabel(BeatmapsetsStrings.ShowScoreboardHeadersCombo.ToUpper(), $"{score.MaxCombo.ToString()}x", score.MaxCombo == score.GetMaximumAchievableCombo(), 60),
new ScoreComponentLabel(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper(), score.DisplayAccuracy, score.Accuracy == 1, 55),
},
Alpha = 0,
}
}
@@ -472,6 +476,21 @@ namespace osu.Game.Screens.SelectV2
innerAvatar.OnLoadComplete += d => d.FadeInFromZero(200);
}
private ColourInfo getHighlightColour(HighlightType? highlightType, float lightenAmount = 0)
{
switch (highlightType)
{
case HighlightType.Own:
return ColourInfo.GradientHorizontal(personal_best_gradient_left.Lighten(lightenAmount), personal_best_gradient_right.Lighten(lightenAmount));
case HighlightType.Friend:
return ColourInfo.GradientHorizontal(colours.Pink1.Lighten(lightenAmount), colours.Pink3.Lighten(lightenAmount));
default:
return Colour4.White;
}
}
protected override void LoadComplete()
{
base.LoadComplete();
@@ -529,12 +548,11 @@ namespace osu.Game.Screens.SelectV2
private void updateState()
{
var lightenedGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0).Lighten(0.2f), backgroundColour.Lighten(0.2f));
var personalBestLightenedGradient = ColourInfo.GradientHorizontal(personal_best_gradient_left.Lighten(0.2f), personal_best_gradient_right.Lighten(0.2f));
foreground.FadeColour(IsHovered ? foregroundColour.Lighten(0.2f) : foregroundColour, transition_duration, Easing.OutQuint);
background.FadeColour(IsHovered ? backgroundColour.Lighten(0.2f) : backgroundColour, transition_duration, Easing.OutQuint);
totalScoreBackground.FadeColour(IsHovered ? lightenedGradient : totalScoreBackgroundGradient, transition_duration, Easing.OutQuint);
personalBestIndicator.FadeColour(IsHovered ? personalBestLightenedGradient : personalBestGradient, transition_duration, Easing.OutQuint);
highlightGradient.FadeColour(getHighlightColour(Highlight, IsHovered ? 0.2f : 0), transition_duration, Easing.OutQuint);
if (IsHovered && currentMode != DisplayMode.Full)
rankLabelOverlay.FadeIn(transition_duration, Easing.OutQuint);
@@ -640,48 +658,50 @@ namespace osu.Game.Screens.SelectV2
private partial class ScoreComponentLabel : Container
{
private readonly (LocalisableString Name, LocalisableString Value) statisticInfo;
private readonly ScoreInfo score;
private readonly LocalisableString name;
private readonly LocalisableString value;
private readonly bool perfect;
private readonly float minWidth;
private FillFlowContainer content = null!;
public override bool Contains(Vector2 screenSpacePos) => content.Contains(screenSpacePos);
public ScoreComponentLabel((LocalisableString Name, LocalisableString Value) statisticInfo, ScoreInfo score)
public ScoreComponentLabel(LocalisableString name, LocalisableString value, bool perfect, float minWidth)
{
this.statisticInfo = statisticInfo;
this.score = score;
this.name = name;
this.value = value;
this.perfect = perfect;
this.minWidth = minWidth;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, OverlayColourProvider colourProvider)
{
AutoSizeAxes = Axes.Both;
OsuSpriteText value;
Child = content = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
Children = new[]
{
new OsuSpriteText
{
Colour = colourProvider.Content2,
Text = statisticInfo.Name,
Text = name,
Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold),
},
value = new OsuSpriteText
new OsuSpriteText
{
// We don't want the value setting the horizontal size, since it leads to wonky accuracy container length,
// since the accuracy is sometimes longer than its name.
BypassAutoSizeAxes = Axes.X,
Text = statisticInfo.Value,
Text = value,
Font = OsuFont.Style.Body,
}
Colour = perfect ? colours.Lime1 : Color4.White,
},
Empty().With(d => d.Width = minWidth),
}
};
if (score.Combo != score.MaxCombo && statisticInfo.Name == BeatmapsetsStrings.ShowScoreboardHeadersCombo)
value.Colour = colours.Lime1;
}
}
@@ -715,5 +735,11 @@ namespace osu.Game.Screens.SelectV2
public LocalisableString TooltipText { get; }
}
public enum HighlightType
{
Own,
Friend,
}
}
}
@@ -3,6 +3,7 @@
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
@@ -12,6 +13,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -88,17 +90,24 @@ namespace osu.Game.Screens.SelectV2
private DrawableDate relativeDate = null!;
private FillFlowContainer statistics = null!;
private readonly Bindable<bool> prefer24HourTime = new Bindable<bool>();
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
private ScoreInfo score = null!;
public ScoreInfo Score
{
get => score;
set
{
absoluteDate.Text = value.Date.ToLocalisableString(@"dd MMMM yyyy h:mm tt");
score = value;
updateAbsoluteDate();
relativeDate.Date = value.Date;
var judgementsStatistics = value.GetStatisticsForDisplay().Select(s =>
@@ -131,7 +140,7 @@ namespace osu.Game.Screens.SelectV2
}
[BackgroundDependencyLoader]
private void load()
private void load(OsuConfigManager configManager)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@@ -205,7 +214,19 @@ namespace osu.Game.Screens.SelectV2
},
},
};
configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime);
}
protected override void LoadComplete()
{
base.LoadComplete();
prefer24HourTime.BindValueChanged(_ => updateAbsoluteDate(), true);
}
private void updateAbsoluteDate()
=> absoluteDate.Text = score.Date.ToLocalTime().ToLocalisableString(prefer24HourTime.Value ? @"d MMMM yyyy HH:mm" : @"d MMMM yyyy h:mm tt");
}
private partial class StatisticRow : CompositeDrawable
@@ -22,6 +22,7 @@ using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Leaderboards;
using osu.Game.Online.Placeholders;
using osu.Game.Overlays;
@@ -60,6 +61,9 @@ namespace osu.Game.Screens.SelectV2
[Resolved]
private ISongSelect? songSelect { get; set; }
[Resolved]
private IAPIProvider api { get; set; } = null!;
private Container<Placeholder> placeholderContainer = null!;
private Placeholder? placeholder;
@@ -255,12 +259,22 @@ namespace osu.Game.Screens.SelectV2
return;
}
LoadComponentsAsync(scores.Select((s, i) => new BeatmapLeaderboardScore(s)
LoadComponentsAsync(scores.Select((s, i) =>
{
Rank = i + 1,
IsPersonalBest = s.OnlineID == userScore?.OnlineID,
SelectedMods = { BindTarget = mods },
Action = () => onLeaderboardScoreClicked(s),
BeatmapLeaderboardScore.HighlightType? highlightType = null;
if (s.OnlineID == userScore?.OnlineID)
highlightType = BeatmapLeaderboardScore.HighlightType.Own;
else if (api.Friends.Any(r => r.TargetID == s.UserID) && Scope.Value != BeatmapLeaderboardScope.Friend)
highlightType = BeatmapLeaderboardScore.HighlightType.Friend;
return new BeatmapLeaderboardScore(s)
{
Rank = i + 1,
Highlight = highlightType,
SelectedMods = { BindTarget = mods },
Action = () => onLeaderboardScoreClicked(s),
};
}), loadedScores =>
{
int delay = 200;
@@ -293,7 +307,7 @@ namespace osu.Game.Screens.SelectV2
personalBestDisplay.FadeIn(600, Easing.OutQuint);
personalBestScoreContainer.Child = new BeatmapLeaderboardScore(userScore)
{
IsPersonalBest = true,
Highlight = BeatmapLeaderboardScore.HighlightType.Own,
Rank = userScore.Position,
SelectedMods = { BindTarget = mods },
Action = () => onLeaderboardScoreClicked(userScore),
@@ -133,7 +133,7 @@ namespace osu.Game.Screens.SelectV2
private OverlayColourProvider colourProvider { get; set; } = null!;
[Resolved]
private SongSelect? songSelect { get; set; }
private ISongSelect? songSelect { get; set; }
public float LineBaseHeight => text.LineBaseHeight;
@@ -196,7 +196,7 @@ namespace osu.Game.Screens.SelectV2
private readonly string[] tags;
private readonly ISongSelect? songSelect;
public TagsOverflowPopover(string[] tags, SongSelect? songSelect)
public TagsOverflowPopover(string[] tags, ISongSelect? songSelect)
{
this.tags = tags;
this.songSelect = songSelect;
+3
View File
@@ -69,6 +69,9 @@ namespace osu.Game.Screens.SelectV2
get => accentColour;
set
{
if (value == accentColour)
return;
accentColour = value;
updateAccentColour();
}
+7 -1
View File
@@ -447,7 +447,13 @@ namespace osu.Game.Screens.SelectV2
// Debounce consideration is to avoid beatmap churn on key repeat selection.
selectionDebounce?.Cancel();
selectionDebounce = Scheduler.AddDelayed(() => Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap), SELECTION_DEBOUNCE);
selectionDebounce = Scheduler.AddDelayed(() =>
{
if (Beatmap.Value.BeatmapInfo.Equals(beatmap))
return;
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap);
}, SELECTION_DEBOUNCE);
}
private bool ensureGlobalBeatmapValid()