mirror of
https://github.com/ppy/osu.git
synced 2026-05-19 18:19:54 +08:00
Compare commits
40 Commits
2025.605.1
...
2025.607.1
+4
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -69,6 +69,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
get => accentColour;
|
||||
set
|
||||
{
|
||||
if (value == accentColour)
|
||||
return;
|
||||
|
||||
accentColour = value;
|
||||
updateAccentColour();
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user