1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-02 00:12:54 +08:00

Merge branch 'master' into osu-ruleset-multi-touch-basic

This commit is contained in:
Bartłomiej Dach 2023-01-19 00:26:57 +01:00 committed by GitHub
commit 2fba80dfd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1244 additions and 170 deletions

View File

@ -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 NUnit.Framework;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public partial class TestSceneManiaModFadeIn : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[TestCase(0.5f)]
[TestCase(0.1f)]
[TestCase(0.7f)]
public void TestCoverage(float coverage) => CreateModTest(new ModTestData { Mod = new ManiaModFadeIn { Coverage = { Value = coverage } }, PassCondition = () => true });
}
}

View File

@ -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 NUnit.Framework;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public partial class TestSceneManiaModHidden : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[TestCase(0.5f)]
[TestCase(0.2f)]
[TestCase(0.8f)]
public void TestCoverage(float coverage) => CreateModTest(new ModTestData { Mod = new ManiaModHidden { Coverage = { Value = coverage } }, PassCondition = () => true });
}
}

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
@ -18,5 +19,13 @@ namespace osu.Game.Rulesets.Mania.Mods
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray();
protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AlongScroll; protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AlongScroll;
public override BindableNumber<float> Coverage { get; } = new BindableFloat(0.5f)
{
Precision = 0.1f,
MinValue = 0.1f,
MaxValue = 0.7f,
Default = 0.5f,
};
} }
} }

View File

@ -5,6 +5,7 @@ using System;
using System.Linq; using System.Linq;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
@ -13,6 +14,14 @@ namespace osu.Game.Rulesets.Mania.Mods
public override LocalisableString Description => @"Keys fade out before you hit them!"; public override LocalisableString Description => @"Keys fade out before you hit them!";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override BindableNumber<float> Coverage { get; } = new BindableFloat(0.5f)
{
Precision = 0.1f,
MinValue = 0.2f,
MaxValue = 0.8f,
Default = 0.5f,
};
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray();
protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll; protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll;

View File

@ -3,8 +3,10 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -22,6 +24,9 @@ namespace osu.Game.Rulesets.Mania.Mods
/// </summary> /// </summary>
protected abstract CoverExpandDirection ExpandDirection { get; } protected abstract CoverExpandDirection ExpandDirection { get; }
[SettingSource("Coverage", "The proportion of playfield height that notes will be hidden for.")]
public abstract BindableNumber<float> Coverage { get; }
public virtual void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset) public virtual void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset)
{ {
ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield; ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield;
@ -36,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
c.RelativeSizeAxes = Axes.Both; c.RelativeSizeAxes = Axes.Both;
c.Direction = ExpandDirection; c.Direction = ExpandDirection;
c.Coverage = 0.5f; c.Coverage = Coverage.Value;
})); }));
} }
} }

View File

@ -41,10 +41,14 @@ namespace osu.Game.Tests.Skins
"Archives/modified-default-20220818.osk", "Archives/modified-default-20220818.osk",
// Covers longest combo counter // Covers longest combo counter
"Archives/modified-default-20221012.osk", "Archives/modified-default-20221012.osk",
// Covers Argon variant of song progress bar
"Archives/modified-argon-20221024.osk",
// Covers TextElement and BeatmapInfoDrawable // Covers TextElement and BeatmapInfoDrawable
"Archives/modified-default-20221102.osk", "Archives/modified-default-20221102.osk",
// Covers BPM counter. // Covers BPM counter.
"Archives/modified-default-20221205.osk" "Archives/modified-default-20221205.osk",
// Covers judgement counter.
"Archives/modified-default-20230117.osk"
}; };
/// <summary> /// <summary>

View File

@ -20,7 +20,9 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
var implementation = skin is LegacySkin var implementation = skin is LegacySkin
? CreateLegacyImplementation() ? CreateLegacyImplementation()
: CreateDefaultImplementation(); : skin is ArgonSkin
? CreateArgonImplementation()
: CreateDefaultImplementation();
implementation.Anchor = Anchor.Centre; implementation.Anchor = Anchor.Centre;
implementation.Origin = Anchor.Centre; implementation.Origin = Anchor.Centre;
@ -29,6 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
protected abstract Drawable CreateDefaultImplementation(); protected abstract Drawable CreateDefaultImplementation();
protected virtual Drawable CreateArgonImplementation() => CreateDefaultImplementation();
protected abstract Drawable CreateLegacyImplementation(); protected abstract Drawable CreateLegacyImplementation();
} }
} }

View File

@ -14,7 +14,7 @@ using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
[TestFixture] [TestFixture]
public partial class TestSceneSongProgressGraph : OsuTestScene public partial class TestSceneDefaultSongProgressGraph : OsuTestScene
{ {
private TestSongProgressGraph graph; private TestSongProgressGraph graph;
@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Gameplay
graph.Objects = objects; graph.Objects = objects;
} }
private partial class TestSongProgressGraph : SongProgressGraph private partial class TestSongProgressGraph : DefaultSongProgressGraph
{ {
public int CreationCount { get; private set; } public int CreationCount { get; private set; }

View File

@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestInputDoesntWorkWhenHUDHidden() public void TestInputDoesntWorkWhenHUDHidden()
{ {
SongProgressBar? getSongProgress() => hudOverlay.ChildrenOfType<SongProgressBar>().SingleOrDefault(); ArgonSongProgress? getSongProgress() => hudOverlay.ChildrenOfType<ArgonSongProgress>().SingleOrDefault();
bool seeked = false; bool seeked = false;
@ -204,8 +204,8 @@ namespace osu.Game.Tests.Visual.Gameplay
Debug.Assert(progress != null); Debug.Assert(progress != null);
progress.ShowHandle = true; progress.Interactive.Value = true;
progress.OnSeek += _ => seeked = true; progress.ChildrenOfType<ArgonSongProgressBar>().Single().OnSeek += _ => seeked = true;
}); });
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);

View File

@ -0,0 +1,144 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD.JudgementCounter;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneJudgementCounter : OsuTestScene
{
private ScoreProcessor scoreProcessor = null!;
private JudgementTally judgementTally = null!;
private TestJudgementCounterDisplay counterDisplay = null!;
private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
private int iteration;
[SetUpSteps]
public void SetupSteps() => AddStep("Create components", () =>
{
var ruleset = CreateRuleset();
Debug.Assert(ruleset != null);
scoreProcessor = new ScoreProcessor(ruleset);
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[] { (typeof(ScoreProcessor), scoreProcessor), (typeof(Ruleset), ruleset) },
Children = new Drawable[]
{
judgementTally = new JudgementTally(),
new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[] { (typeof(JudgementTally), judgementTally) },
Child = counterDisplay = new TestJudgementCounterDisplay
{
Margin = new MarginPadding { Top = 100 },
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
}
}
},
};
});
protected override Ruleset CreateRuleset() => new ManiaRuleset();
private void applyOneJudgement(HitResult result)
{
lastJudgementResult.Value = new OsuJudgementResult(new HitObject
{
StartTime = iteration * 10000
}, new OsuJudgement())
{
Type = result,
};
scoreProcessor.ApplyResult(lastJudgementResult.Value);
iteration++;
}
[Test]
public void TestAddJudgementsToCounters()
{
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Great), 2);
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Miss), 2);
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Meh), 2);
}
[Test]
public void TestAddWhilstHidden()
{
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.LargeTickHit), 2);
AddAssert("Check value added whilst hidden", () => hiddenCount() == 2);
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
}
[Test]
public void TestChangeFlowDirection()
{
AddStep("Set direction vertical", () => counterDisplay.FlowDirection.Value = Direction.Vertical);
AddStep("Set direction horizontal", () => counterDisplay.FlowDirection.Value = Direction.Horizontal);
}
[Test]
public void TestToggleJudgementNames()
{
AddStep("Hide judgement names", () => counterDisplay.ShowJudgementNames.Value = false);
AddWaitStep("wait some", 2);
AddAssert("Assert hidden", () => counterDisplay.CounterFlow.Children.First().ResultName.Alpha == 0);
AddStep("Hide judgement names", () => counterDisplay.ShowJudgementNames.Value = true);
AddWaitStep("wait some", 2);
AddAssert("Assert shown", () => counterDisplay.CounterFlow.Children.First().ResultName.Alpha == 1);
}
[Test]
public void TestHideMaxValue()
{
AddStep("Hide max judgement", () => counterDisplay.ShowMaxJudgement.Value = false);
AddWaitStep("wait some", 2);
AddAssert("Check max hidden", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().First().Alpha == 0);
AddStep("Show max judgement", () => counterDisplay.ShowMaxJudgement.Value = true);
}
[Test]
public void TestCycleDisplayModes()
{
AddStep("Show basic judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.Simple);
AddWaitStep("wait some", 2);
AddAssert("Check only basic", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().Last().Alpha == 0);
AddStep("Show normal judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.Normal);
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
AddWaitStep("wait some", 2);
AddAssert("Check all visible", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().Last().Alpha == 1);
}
private int hiddenCount()
{
var num = counterDisplay.CounterFlow.Children.First(child => child.Result.Type == HitResult.LargeTickHit);
return num.Result.ResultCount.Value;
}
private partial class TestJudgementCounterDisplay : JudgementCounterDisplay
{
public new FillFlowContainer<JudgementCounter> CounterFlow => base.CounterFlow;
}
}
}

View File

@ -2,14 +2,14 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -28,50 +28,62 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time)); FrameStabilityContainer frameStabilityContainer;
Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time)
{
Child = frameStabilityContainer = new FrameStabilityContainer
{
MaxCatchUpFrames = 1
}
});
Dependencies.CacheAs<IGameplayClock>(gameplayClockContainer); Dependencies.CacheAs<IGameplayClock>(gameplayClockContainer);
Dependencies.CacheAs<IFrameStableClock>(frameStabilityContainer);
} }
[SetUpSteps] [SetUpSteps]
public void SetupSteps() public void SetupSteps()
{ {
AddStep("reset clock", () => gameplayClockContainer.Reset()); AddStep("reset clock", () => gameplayClockContainer.Reset());
AddStep("set hit objects", setHitObjects); AddStep("set hit objects", () => this.ChildrenOfType<SongProgress>().ForEach(progress => progress.Objects = Beatmap.Value.Beatmap.HitObjects));
AddStep("hook seeking", () =>
{
applyToDefaultProgress(d => d.ChildrenOfType<DefaultSongProgressBar>().Single().OnSeek += t => gameplayClockContainer.Seek(t));
applyToArgonProgress(d => d.ChildrenOfType<ArgonSongProgressBar>().Single().OnSeek += t => gameplayClockContainer.Seek(t));
});
AddStep("seek to intro", () => gameplayClockContainer.Seek(skip_target_time));
AddStep("start", () => gameplayClockContainer.Start());
} }
[Test] [Test]
public void TestDisplay() public void TestBasic()
{ {
AddStep("seek to intro", () => gameplayClockContainer.Seek(skip_target_time)); AddToggleStep("toggle seeking", b =>
AddStep("start", gameplayClockContainer.Start); {
applyToDefaultProgress(s => s.Interactive.Value = b);
applyToArgonProgress(s => s.Interactive.Value = b);
});
AddToggleStep("toggle graph", b =>
{
applyToDefaultProgress(s => s.ShowGraph.Value = b);
applyToArgonProgress(s => s.ShowGraph.Value = b);
});
AddStep("stop", gameplayClockContainer.Stop); AddStep("stop", gameplayClockContainer.Stop);
} }
[Test] private void applyToArgonProgress(Action<ArgonSongProgress> action) =>
public void TestToggleSeeking() this.ChildrenOfType<ArgonSongProgress>().ForEach(action);
{
void applyToDefaultProgress(Action<DefaultSongProgress> action) =>
this.ChildrenOfType<DefaultSongProgress>().ForEach(action);
AddStep("allow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = true)); private void applyToDefaultProgress(Action<DefaultSongProgress> action) =>
AddStep("hide graph", () => applyToDefaultProgress(s => s.ShowGraph.Value = false)); this.ChildrenOfType<DefaultSongProgress>().ForEach(action);
AddStep("disallow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = false));
AddStep("allow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = true));
AddStep("show graph", () => applyToDefaultProgress(s => s.ShowGraph.Value = true));
}
private void setHitObjects()
{
var objects = new List<HitObject>();
for (double i = 0; i < 5000; i++)
objects.Add(new HitObject { StartTime = i });
this.ChildrenOfType<SongProgress>().ForEach(progress => progress.Objects = objects);
}
protected override Drawable CreateDefaultImplementation() => new DefaultSongProgress(); protected override Drawable CreateDefaultImplementation() => new DefaultSongProgress();
protected override Drawable CreateArgonImplementation() => new ArgonSongProgress();
protected override Drawable CreateLegacyImplementation() => new LegacySongProgress(); protected override Drawable CreateLegacyImplementation() => new LegacySongProgress();
} }
} }

View File

@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("all interactive elements removed", () => this.ChildrenOfType<Player>().All(p => AddUntilStep("all interactive elements removed", () => this.ChildrenOfType<Player>().All(p =>
!p.ChildrenOfType<PlayerSettingsOverlay>().Any() && !p.ChildrenOfType<PlayerSettingsOverlay>().Any() &&
!p.ChildrenOfType<HoldForMenuButton>().Any() && !p.ChildrenOfType<HoldForMenuButton>().Any() &&
p.ChildrenOfType<SongProgressBar>().SingleOrDefault()?.ShowHandle == false)); p.ChildrenOfType<ArgonSongProgressBar>().SingleOrDefault()?.Interactive == false));
AddStep("restore config hud visibility", () => config.SetValue(OsuSetting.HUDVisibilityMode, originalConfigValue)); AddStep("restore config hud visibility", () => config.SetValue(OsuSetting.HUDVisibilityMode, originalConfigValue));
} }

View File

@ -580,10 +580,9 @@ namespace osu.Game.Tests.Visual.SongSelect
/// Ensures stability is maintained on different sort modes for items with equal properties. /// Ensures stability is maintained on different sort modes for items with equal properties.
/// </summary> /// </summary>
[Test] [Test]
public void TestSortingStability() public void TestSortingStabilityDateAdded()
{ {
var sets = new List<BeatmapSetInfo>(); var sets = new List<BeatmapSetInfo>();
int idOffset = 0;
AddStep("Populuate beatmap sets", () => AddStep("Populuate beatmap sets", () =>
{ {
@ -593,38 +592,34 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
var set = TestResources.CreateTestBeatmapSetInfo(); var set = TestResources.CreateTestBeatmapSetInfo();
set.DateAdded = DateTimeOffset.FromUnixTimeSeconds(i);
// only need to set the first as they are a shared reference. // only need to set the first as they are a shared reference.
var beatmap = set.Beatmaps.First(); var beatmap = set.Beatmaps.First();
beatmap.Metadata.Artist = $"artist {i / 2}"; beatmap.Metadata.Artist = "a";
beatmap.Metadata.Title = $"title {9 - i}"; beatmap.Metadata.Title = "b";
sets.Add(set); sets.Add(set);
} }
idOffset = sets.First().OnlineID;
}); });
loadBeatmaps(sets); loadBeatmaps(sets);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + index).All(b => b));
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false)); AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
AddAssert("Items are in reverse order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + sets.Count - index - 1).All(b => b)); AddAssert("Items remain in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddAssert("Items reset to original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + index).All(b => b)); AddAssert("Items remain in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
} }
/// <summary> /// <summary>
/// Ensures stability is maintained on different sort modes while a new item is added to the carousel. /// Ensures stability is maintained on different sort modes while a new item is added to the carousel.
/// </summary> /// </summary>
[Test] [Test]
public void TestSortingStabilityWithNewItems() public void TestSortingStabilityWithRemovedAndReaddedItem()
{ {
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>(); List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
int idOffset = 0;
AddStep("Populuate beatmap sets", () => AddStep("Populuate beatmap sets", () =>
{ {
@ -640,16 +635,68 @@ namespace osu.Game.Tests.Visual.SongSelect
beatmap.Metadata.Artist = "same artist"; beatmap.Metadata.Artist = "same artist";
beatmap.Metadata.Title = "same title"; beatmap.Metadata.Title = "same title";
// testing the case where DateAdded happens to equal (quite rare).
set.DateAdded = DateTimeOffset.UnixEpoch;
sets.Add(set); sets.Add(set);
} }
idOffset = sets.First().OnlineID;
}); });
Guid[] originalOrder = null!;
loadBeatmaps(sets); loadBeatmaps(sets);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
assertOriginalOrderMaintained();
AddAssert("Items in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
AddStep("Save order", () => originalOrder = carousel.BeatmapSets.Select(s => s.ID).ToArray());
AddStep("Remove item", () => carousel.RemoveBeatmapSet(sets[1]));
AddStep("Re-add item", () => carousel.UpdateBeatmapSet(sets[1]));
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
}
/// <summary>
/// Ensures stability is maintained on different sort modes while a new item is added to the carousel.
/// </summary>
[Test]
public void TestSortingStabilityWithNewItems()
{
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
AddStep("Populuate beatmap sets", () =>
{
sets.Clear();
for (int i = 0; i < 3; i++)
{
var set = TestResources.CreateTestBeatmapSetInfo(3);
// only need to set the first as they are a shared reference.
var beatmap = set.Beatmaps.First();
beatmap.Metadata.Artist = "same artist";
beatmap.Metadata.Title = "same title";
// testing the case where DateAdded happens to equal (quite rare).
set.DateAdded = DateTimeOffset.UnixEpoch;
sets.Add(set);
}
});
Guid[] originalOrder = null!;
loadBeatmaps(sets);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddAssert("Items in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
AddStep("Save order", () => originalOrder = carousel.BeatmapSets.Select(s => s.ID).ToArray());
AddStep("Add new item", () => AddStep("Add new item", () =>
{ {
@ -661,19 +708,18 @@ namespace osu.Game.Tests.Visual.SongSelect
beatmap.Metadata.Artist = "same artist"; beatmap.Metadata.Artist = "same artist";
beatmap.Metadata.Title = "same title"; beatmap.Metadata.Title = "same title";
set.DateAdded = DateTimeOffset.FromUnixTimeSeconds(1);
carousel.UpdateBeatmapSet(set); carousel.UpdateBeatmapSet(set);
// add set to expected ordering
originalOrder = originalOrder.Prepend(set.ID).ToArray();
}); });
assertOriginalOrderMaintained(); AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false)); AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
assertOriginalOrderMaintained(); AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
void assertOriginalOrderMaintained()
{
AddAssert("Items remain in original order",
() => carousel.BeatmapSets.Select(s => s.OnlineID), () => Is.EqualTo(carousel.BeatmapSets.Select((set, index) => idOffset + index)));
}
} }
[Test] [Test]

View File

@ -48,17 +48,14 @@ namespace osu.Game.Graphics.UserInterface
} }
} }
private Colour4[] tierColours; private IReadOnlyList<Colour4> tierColours;
public Colour4[] TierColours public IReadOnlyList<Colour4> TierColours
{ {
get => tierColours; get => tierColours;
set set
{ {
if (value.Length == 0 || value == tierColours) tierCount = value.Count;
return;
tierCount = value.Length;
tierColours = value; tierColours = value;
graphNeedsUpdate = true; graphNeedsUpdate = true;

View File

@ -19,6 +19,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"join the real-time discussion"); public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"join the real-time discussion");
/// <summary>
/// "Mention"
/// </summary>
public static LocalisableString MentionUser => new TranslatableString(getKey(@"mention_user"), @"Mention");
private static string getKey(string key) => $"{prefix}:{key}"; private static string getKey(string key) => $"{prefix}:{key}";
} }
} }

View File

@ -25,6 +25,7 @@ namespace osu.Game.Online.Chat
/// </summary> /// </summary>
public partial class StandAloneChatDisplay : CompositeDrawable public partial class StandAloneChatDisplay : CompositeDrawable
{ {
[Cached]
public readonly Bindable<Channel> Channel = new Bindable<Channel>(); public readonly Bindable<Channel> Channel = new Bindable<Channel>();
protected readonly ChatTextBox TextBox; protected readonly ChatTextBox TextBox;

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -24,6 +25,7 @@ using osu.Game.Online.Chat;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using ChatStrings = osu.Game.Localisation.ChatStrings;
namespace osu.Game.Overlays.Chat namespace osu.Game.Overlays.Chat
{ {
@ -65,6 +67,9 @@ namespace osu.Game.Overlays.Chat
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private UserProfileOverlay? profileOverlay { get; set; } private UserProfileOverlay? profileOverlay { get; set; }
[Resolved]
private Bindable<Channel?>? currentChannel { get; set; }
private readonly APIUser user; private readonly APIUser user;
private readonly OsuSpriteText drawableText; private readonly OsuSpriteText drawableText;
@ -156,6 +161,14 @@ namespace osu.Game.Overlays.Chat
if (!user.Equals(api.LocalUser.Value)) if (!user.Equals(api.LocalUser.Value))
items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel)); items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel));
if (currentChannel?.Value != null)
{
items.Add(new OsuMenuItem(ChatStrings.MentionUser, MenuItemType.Standard, () =>
{
currentChannel.Value.TextBoxMessage.Value += $"@{user.Username} ";
}));
}
return items.ToArray(); return items.ToArray();
} }
} }

View File

@ -0,0 +1,117 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonSongProgress : SongProgress
{
private readonly SongProgressInfo info;
private readonly ArgonSongProgressGraph graph;
private readonly ArgonSongProgressBar bar;
private readonly Container graphContainer;
private const float bar_height = 10;
[SettingSource("Show difficulty graph", "Whether a graph displaying difficulty throughout the beatmap should be shown")]
public Bindable<bool> ShowGraph { get; } = new BindableBool(true);
[Resolved]
private Player? player { get; set; }
public ArgonSongProgress()
{
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
Masking = true;
CornerRadius = 5;
Children = new Drawable[]
{
info = new SongProgressInfo
{
Origin = Anchor.TopLeft,
Name = "Info",
Anchor = Anchor.TopLeft,
RelativeSizeAxes = Axes.X,
ShowProgress = false
},
bar = new ArgonSongProgressBar(bar_height)
{
Name = "Seek bar",
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
OnSeek = time => player?.Seek(time),
},
graphContainer = new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Masking = true,
CornerRadius = 5,
Child = graph = new ArgonSongProgressGraph
{
Name = "Difficulty graph",
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive
},
RelativeSizeAxes = Axes.X,
},
};
RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
private void load()
{
info.TextColour = Colour4.White;
info.Font = OsuFont.Torus.With(size: 18, weight: FontWeight.Bold);
}
protected override void LoadComplete()
{
base.LoadComplete();
Interactive.BindValueChanged(_ => bar.Interactive = Interactive.Value, true);
ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true);
}
protected override void UpdateObjects(IEnumerable<HitObject> objects)
{
graph.Objects = objects;
info.StartTime = bar.StartTime = FirstHitTime;
info.EndTime = bar.EndTime = LastHitTime;
}
private void updateGraphVisibility()
{
graph.FadeTo(ShowGraph.Value ? 1 : 0, 200, Easing.In);
bar.ShowBackground = !ShowGraph.Value;
}
protected override void Update()
{
base.Update();
Height = bar.Height + bar_height + info.Height;
graphContainer.Height = bar.Height;
}
protected override void UpdateProgress(double progress, bool isIntro)
{
bar.TrackTime = GameplayClock.CurrentTime;
if (isIntro)
bar.CurrentTime = 0;
else
bar.CurrentTime = FrameStableClock.CurrentTime;
}
}
}

View File

@ -0,0 +1,266 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonSongProgressBar : SliderBar<double>
{
public Action<double>? OnSeek { get; set; }
// Parent will handle restricting the area of valid input.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
private readonly float barHeight;
private readonly RoundedBar playfieldBar;
private readonly RoundedBar catchupBar;
private readonly Box background;
private readonly BindableBool showBackground = new BindableBool();
private readonly ColourInfo mainColour;
private readonly ColourInfo mainColourDarkened;
private ColourInfo catchUpColour;
private ColourInfo catchUpColourDarkened;
public bool ShowBackground
{
get => showBackground.Value;
set => showBackground.Value = value;
}
public double StartTime
{
private get => CurrentNumber.MinValue;
set => CurrentNumber.MinValue = value;
}
public double EndTime
{
private get => CurrentNumber.MaxValue;
set => CurrentNumber.MaxValue = value;
}
public double CurrentTime
{
private get => CurrentNumber.Value;
set => CurrentNumber.Value = value;
}
public double TrackTime
{
private get => currentTrackTime.Value;
set => currentTrackTime.Value = value;
}
private double length => EndTime - StartTime;
private readonly BindableNumber<double> currentTrackTime;
public bool Interactive { get; set; }
public ArgonSongProgressBar(float barHeight)
{
currentTrackTime = new BindableDouble();
setupAlternateValue();
StartTime = 0;
EndTime = 1;
RelativeSizeAxes = Axes.X;
Height = this.barHeight = barHeight;
CornerRadius = 5;
Masking = true;
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Colour = Colour4.White.Darken(1 + 1 / 4f)
},
catchupBar = new RoundedBar
{
Name = "Audio bar",
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
CornerRadius = 5,
AlwaysPresent = true,
RelativeSizeAxes = Axes.Both
},
playfieldBar = new RoundedBar
{
Name = "Playfield bar",
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
CornerRadius = 5,
AccentColour = mainColour = Color4.White,
RelativeSizeAxes = Axes.Both
},
};
mainColourDarkened = Colour4.White.Darken(1 / 3f);
}
private void setupAlternateValue()
{
CurrentNumber.MaxValueChanged += v => currentTrackTime.MaxValue = v;
CurrentNumber.MinValueChanged += v => currentTrackTime.MinValue = v;
CurrentNumber.PrecisionChanged += v => currentTrackTime.Precision = v;
}
private float normalizedReference
{
get
{
if (EndTime - StartTime == 0)
return 1;
return (float)((TrackTime - StartTime) / length);
}
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
catchUpColour = colours.BlueLight;
catchUpColourDarkened = colours.BlueDark;
showBackground.BindValueChanged(_ => updateBackground(), true);
}
private void updateBackground()
{
background.FadeTo(showBackground.Value ? 1 / 4f : 0, 200, Easing.In);
playfieldBar.TransformTo(nameof(playfieldBar.AccentColour), ShowBackground ? mainColour : mainColourDarkened, 200, Easing.In);
}
protected override bool OnHover(HoverEvent e)
{
if (Interactive)
this.ResizeHeightTo(barHeight * 3.5f, 200, Easing.Out);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
this.ResizeHeightTo(barHeight, 800, Easing.OutQuint);
base.OnHoverLost(e);
}
protected override void UpdateValue(float value)
{
// Handled in Update
}
protected override void Update()
{
base.Update();
playfieldBar.Length = (float)Interpolation.Lerp(playfieldBar.Length, NormalizedValue, Math.Clamp(Time.Elapsed / 40, 0, 1));
catchupBar.Length = (float)Interpolation.Lerp(catchupBar.Length, normalizedReference, Math.Clamp(Time.Elapsed / 40, 0, 1));
if (TrackTime < CurrentTime)
ChangeChildDepth(catchupBar, -1);
else
ChangeChildDepth(catchupBar, 0);
float timeDelta = (float)(Math.Abs(CurrentTime - TrackTime));
const float colour_transition_threshold = 20000;
catchupBar.AccentColour = Interpolation.ValueAt(
Math.Min(timeDelta, colour_transition_threshold),
ShowBackground ? mainColour : mainColourDarkened,
ShowBackground ? catchUpColour : catchUpColourDarkened,
0, colour_transition_threshold,
Easing.OutQuint);
catchupBar.Alpha = Math.Max(1, catchupBar.Length);
}
private ScheduledDelegate? scheduledSeek;
protected override void OnUserChange(double value)
{
scheduledSeek?.Cancel();
scheduledSeek = Schedule(() =>
{
if (Interactive)
OnSeek?.Invoke(value);
});
}
private partial class RoundedBar : Container
{
private readonly Box fill;
private readonly Container mask;
private float length;
public RoundedBar()
{
Masking = true;
Children = new[]
{
mask = new Container
{
Masking = true,
RelativeSizeAxes = Axes.Y,
Size = new Vector2(1),
Child = fill = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.White
}
}
};
}
public float Length
{
get => length;
set
{
length = value;
mask.Width = value * DrawWidth;
fill.Width = value * DrawWidth;
}
}
public new float CornerRadius
{
get => base.CornerRadius;
set
{
base.CornerRadius = value;
mask.CornerRadius = value;
}
}
public ColourInfo AccentColour
{
get => fill.Colour;
set => fill.Colour = value;
}
}
}
}

View File

@ -0,0 +1,64 @@
// 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.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonSongProgressGraph : SegmentedGraph<int>
{
private IEnumerable<HitObject>? objects;
public IEnumerable<HitObject> Objects
{
set
{
objects = value;
const int granularity = 200;
int[] values = new int[granularity];
if (!objects.Any())
return;
double firstHit = objects.First().StartTime;
double lastHit = objects.Max(o => o.GetEndTime());
if (lastHit == 0)
lastHit = objects.Last().StartTime;
double interval = (lastHit - firstHit + 1) / granularity;
foreach (var h in objects)
{
double endTime = h.GetEndTime();
Debug.Assert(endTime >= h.StartTime);
int startRange = (int)((h.StartTime - firstHit) / interval);
int endRange = (int)((endTime - firstHit) / interval);
for (int i = startRange; i <= endRange; i++)
values[i]++;
}
Values = values;
}
}
public ArgonSongProgressGraph()
: base(5)
{
var colours = new List<Colour4>();
for (int i = 0; i < 5; i++)
colours.Add(Colour4.White.Darken(1 + 1 / 5f).Opacity(1 / 5f));
TierColours = colours;
}
}
}

View File

@ -15,14 +15,12 @@ namespace osu.Game.Screens.Play.HUD.ClicksPerSecond
[Resolved] [Resolved]
private IGameplayClock gameplayClock { get; set; } = null!; private IGameplayClock gameplayClock { get; set; } = null!;
[Resolved(canBeNull: true)] [Resolved]
private DrawableRuleset? drawableRuleset { get; set; } private IFrameStableClock? frameStableClock { get; set; }
public int Value { get; private set; } public int Value { get; private set; }
// Even though `FrameStabilityContainer` caches as a `GameplayClock`, we need to check it directly via `drawableRuleset` private IGameplayClock clock => frameStableClock ?? gameplayClock;
// as this calculator is not contained within the `FrameStabilityContainer` and won't see the dependency.
private IGameplayClock clock => drawableRuleset?.FrameStableClock ?? gameplayClock;
public ClicksPerSecondCalculator() public ClicksPerSecondCalculator()
{ {

View File

@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osuTK; using osuTK;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
@ -23,27 +22,16 @@ namespace osu.Game.Screens.Play.HUD
private const float transition_duration = 200; private const float transition_duration = 200;
private readonly SongProgressBar bar; private readonly DefaultSongProgressBar bar;
private readonly SongProgressGraph graph; private readonly DefaultSongProgressGraph graph;
private readonly SongProgressInfo info; private readonly SongProgressInfo info;
/// <summary>
/// Whether seeking is allowed and the progress bar should be shown.
/// </summary>
public readonly Bindable<bool> AllowSeeking = new Bindable<bool>();
[SettingSource("Show difficulty graph", "Whether a graph displaying difficulty throughout the beatmap should be shown")] [SettingSource("Show difficulty graph", "Whether a graph displaying difficulty throughout the beatmap should be shown")]
public Bindable<bool> ShowGraph { get; } = new BindableBool(true); public Bindable<bool> ShowGraph { get; } = new BindableBool(true);
public override bool HandleNonPositionalInput => AllowSeeking.Value;
public override bool HandlePositionalInput => AllowSeeking.Value;
[Resolved] [Resolved]
private Player? player { get; set; } private Player? player { get; set; }
[Resolved]
private DrawableRuleset? drawableRuleset { get; set; }
public DefaultSongProgress() public DefaultSongProgress()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@ -58,7 +46,7 @@ namespace osu.Game.Screens.Play.HUD
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
}, },
graph = new SongProgressGraph graph = new DefaultSongProgressGraph
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
@ -66,7 +54,7 @@ namespace osu.Game.Screens.Play.HUD
Height = graph_height, Height = graph_height,
Margin = new MarginPadding { Bottom = bottom_bar_height }, Margin = new MarginPadding { Bottom = bottom_bar_height },
}, },
bar = new SongProgressBar(bottom_bar_height, graph_height, handle_size) bar = new DefaultSongProgressBar(bottom_bar_height, graph_height, handle_size)
{ {
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
@ -75,34 +63,18 @@ namespace osu.Game.Screens.Play.HUD
}; };
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
base.LoadComplete();
if (drawableRuleset != null)
{
if (player?.Configuration.AllowUserInteraction == true)
((IBindable<bool>)AllowSeeking).BindTo(drawableRuleset.HasReplayLoaded);
}
graph.FillColour = bar.FillColour = colours.BlueLighter; graph.FillColour = bar.FillColour = colours.BlueLighter;
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
AllowSeeking.BindValueChanged(_ => updateBarVisibility(), true); Interactive.BindValueChanged(_ => updateBarVisibility(), true);
ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true);
}
protected override void PopIn() base.LoadComplete();
{
this.FadeIn(500, Easing.OutQuint);
}
protected override void PopOut()
{
this.FadeOut(100);
} }
protected override void UpdateObjects(IEnumerable<HitObject> objects) protected override void UpdateObjects(IEnumerable<HitObject> objects)
@ -133,7 +105,7 @@ namespace osu.Game.Screens.Play.HUD
private void updateBarVisibility() private void updateBarVisibility()
{ {
bar.ShowHandle = AllowSeeking.Value; bar.Interactive = Interactive.Value;
updateInfoMargin(); updateInfoMargin();
} }
@ -150,7 +122,7 @@ namespace osu.Game.Screens.Play.HUD
private void updateInfoMargin() private void updateInfoMargin()
{ {
float finalMargin = bottom_bar_height + (AllowSeeking.Value ? handle_size.Y : 0) + (ShowGraph.Value ? graph_height : 0); float finalMargin = bottom_bar_height + (Interactive.Value ? handle_size.Y : 0) + (ShowGraph.Value ? graph_height : 0);
info.TransformTo(nameof(info.Margin), new MarginPadding { Bottom = finalMargin }, transition_duration, Easing.In); info.TransformTo(nameof(info.Margin), new MarginPadding { Bottom = finalMargin }, transition_duration, Easing.In);
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -15,17 +13,17 @@ using osu.Framework.Threading;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public partial class SongProgressBar : SliderBar<double> public partial class DefaultSongProgressBar : SliderBar<double>
{ {
public Action<double> OnSeek; /// <summary>
/// Action which is invoked when a seek is requested, with the proposed millisecond value for the seek operation.
/// </summary>
public Action<double>? OnSeek { get; set; }
private readonly Box fill; /// <summary>
private readonly Container handleBase; /// Whether the progress bar should allow interaction, ie. to perform seek operations.
private readonly Container handleContainer; /// </summary>
public bool Interactive
private bool showHandle;
public bool ShowHandle
{ {
get => showHandle; get => showHandle;
set set
@ -59,7 +57,13 @@ namespace osu.Game.Screens.Play.HUD
set => CurrentNumber.Value = value; set => CurrentNumber.Value = value;
} }
public SongProgressBar(float barHeight, float handleBarHeight, Vector2 handleSize) private readonly Box fill;
private readonly Container handleBase;
private readonly Container handleContainer;
private bool showHandle;
public DefaultSongProgressBar(float barHeight, float handleBarHeight, Vector2 handleSize)
{ {
CurrentNumber.MinValue = 0; CurrentNumber.MinValue = 0;
CurrentNumber.MaxValue = 1; CurrentNumber.MaxValue = 1;
@ -142,7 +146,7 @@ namespace osu.Game.Screens.Play.HUD
handleBase.X = newX; handleBase.X = newX;
} }
private ScheduledDelegate scheduledSeek; private ScheduledDelegate? scheduledSeek;
protected override void OnUserChange(double value) protected override void OnUserChange(double value)
{ {

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public partial class SongProgressGraph : SquareGraph public partial class DefaultSongProgressGraph : SquareGraph
{ {
private IEnumerable<HitObject> objects; private IEnumerable<HitObject> objects;

View File

@ -0,0 +1,82 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Screens.Play.HUD.JudgementCounter
{
public partial class JudgementCounter : VisibilityContainer
{
public BindableBool ShowName = new BindableBool();
public Bindable<FillDirection> Direction = new Bindable<FillDirection>();
public readonly JudgementTally.JudgementCount Result;
public JudgementCounter(JudgementTally.JudgementCount result) => Result = result;
public OsuSpriteText ResultName = null!;
private FillFlowContainer flowContainer = null!;
private JudgementRollingCounter counter = null!;
[BackgroundDependencyLoader]
private void load(OsuColour colours, IBindable<RulesetInfo> ruleset)
{
AutoSizeAxes = Axes.Both;
InternalChild = flowContainer = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
counter = new JudgementRollingCounter
{
Current = Result.ResultCount
},
ResultName = new OsuSpriteText
{
Alpha = 0,
Font = OsuFont.Numeric.With(size: 8),
Text = ruleset.Value.CreateInstance().GetDisplayNameForHitResult(Result.Type)
}
}
};
var result = Result.Type;
Colour = result.IsBasic() ? colours.ForHitResult(Result.Type) : !result.IsBonus() ? colours.PurpleLight : colours.PurpleLighter;
}
protected override void LoadComplete()
{
ShowName.BindValueChanged(value =>
ResultName.FadeTo(value.NewValue ? 1 : 0, JudgementCounterDisplay.TRANSFORM_DURATION, Easing.OutQuint), true);
Direction.BindValueChanged(direction =>
{
flowContainer.Direction = direction.NewValue;
changeAnchor(direction.NewValue == FillDirection.Vertical ? Anchor.TopLeft : Anchor.BottomLeft);
void changeAnchor(Anchor anchor) => counter.Anchor = ResultName.Anchor = counter.Origin = ResultName.Origin = anchor;
}, true);
base.LoadComplete();
}
protected override void PopIn() => this.FadeIn(JudgementCounterDisplay.TRANSFORM_DURATION, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(100);
private sealed partial class JudgementRollingCounter : RollingCounter<int>
{
protected override OsuSpriteText CreateSpriteText()
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true, size: 16));
}
}
}

View File

@ -0,0 +1,139 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Play.HUD.JudgementCounter
{
public partial class JudgementCounterDisplay : CompositeDrawable, ISkinnableDrawable
{
public const int TRANSFORM_DURATION = 250;
public bool UsesFixedAnchor { get; set; }
[SettingSource("Display mode")]
public Bindable<DisplayMode> Mode { get; set; } = new Bindable<DisplayMode>();
[SettingSource("Counter direction")]
public Bindable<Direction> FlowDirection { get; set; } = new Bindable<Direction>();
[SettingSource("Show judgement names")]
public BindableBool ShowJudgementNames { get; set; } = new BindableBool(true);
[SettingSource("Show max judgement")]
public BindableBool ShowMaxJudgement { get; set; } = new BindableBool(true);
[Resolved]
private JudgementTally tally { get; set; } = null!;
protected FillFlowContainer<JudgementCounter> CounterFlow = null!;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChild = CounterFlow = new FillFlowContainer<JudgementCounter>
{
Direction = getFillDirection(FlowDirection.Value),
Spacing = new Vector2(10),
AutoSizeAxes = Axes.Both
};
foreach (var result in tally.Results)
CounterFlow.Add(createCounter(result));
}
protected override void LoadComplete()
{
base.LoadComplete();
FlowDirection.BindValueChanged(direction =>
{
var convertedDirection = getFillDirection(direction.NewValue);
CounterFlow.Direction = convertedDirection;
foreach (var counter in CounterFlow.Children)
counter.Direction.Value = convertedDirection;
}, true);
Mode.BindValueChanged(_ => updateMode(), true);
ShowMaxJudgement.BindValueChanged(value =>
{
var firstChild = CounterFlow.Children.FirstOrDefault();
firstChild.FadeTo(value.NewValue ? 1 : 0, TRANSFORM_DURATION, Easing.OutQuint);
}, true);
}
private void updateMode()
{
foreach (var counter in CounterFlow.Children)
{
if (shouldShow(counter))
counter.Show();
else
counter.Hide();
}
bool shouldShow(JudgementCounter counter)
{
if (counter.Result.Type.IsBasic())
return true;
switch (Mode.Value)
{
case DisplayMode.Simple:
return false;
case DisplayMode.Normal:
return !counter.Result.Type.IsBonus();
case DisplayMode.All:
return true;
default:
throw new ArgumentOutOfRangeException();
}
}
}
private FillDirection getFillDirection(Direction flow)
{
switch (flow)
{
case Direction.Horizontal:
return FillDirection.Horizontal;
case Direction.Vertical:
return FillDirection.Vertical;
default:
throw new ArgumentOutOfRangeException(nameof(flow), flow, @"Unsupported direction");
}
}
private JudgementCounter createCounter(JudgementTally.JudgementCount info) =>
new JudgementCounter(info)
{
State = { Value = Visibility.Hidden },
ShowName = { BindTarget = ShowJudgementNames }
};
public enum DisplayMode
{
Simple,
Normal,
All
}
}
}

View File

@ -0,0 +1,60 @@
// 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;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Screens.Play.HUD.JudgementCounter
{
/// <summary>
/// Keeps track of judgements for a current play session, exposing bindable counts which can
/// be used for display purposes.
/// </summary>
public partial class JudgementTally : CompositeDrawable
{
[Resolved]
private ScoreProcessor scoreProcessor { get; set; } = null!;
public List<JudgementCount> Results = new List<JudgementCount>();
[BackgroundDependencyLoader]
private void load(IBindable<RulesetInfo> ruleset)
{
foreach (var result in ruleset.Value.CreateInstance().GetHitResults())
{
Results.Add(new JudgementCount
{
Type = result.result,
ResultCount = new BindableInt()
});
}
}
protected override void LoadComplete()
{
base.LoadComplete();
scoreProcessor.NewJudgement += judgement => updateCount(judgement, false);
scoreProcessor.JudgementReverted += judgement => updateCount(judgement, true);
}
private void updateCount(JudgementResult judgement, bool revert)
{
foreach (JudgementCount result in Results.Where(result => result.Type == judgement.Type))
result.ResultCount.Value = revert ? result.ResultCount.Value - 1 : result.ResultCount.Value + 1;
}
public struct JudgementCount
{
public HitResult Type { get; set; }
public BindableInt ResultCount { get; set; }
}
}
}

View File

@ -4,6 +4,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -16,19 +18,33 @@ namespace osu.Game.Screens.Play.HUD
{ {
// Some implementations of this element allow seeking during gameplay playback. // Some implementations of this element allow seeking during gameplay playback.
// Set a sane default of never handling input to override the behaviour provided by OverlayContainer. // Set a sane default of never handling input to override the behaviour provided by OverlayContainer.
public override bool HandleNonPositionalInput => false; public override bool HandleNonPositionalInput => Interactive.Value;
public override bool HandlePositionalInput => false; public override bool HandlePositionalInput => Interactive.Value;
protected override bool BlockScrollInput => false; protected override bool BlockScrollInput => false;
/// <summary>
/// Whether interaction should be allowed (ie. seeking). If <c>false</c>, interaction controls will not be displayed.
/// </summary>
/// <remarks>
/// By default, this will be automatically decided based on the gameplay state.
/// </remarks>
public readonly Bindable<bool> Interactive = new Bindable<bool>();
public bool UsesFixedAnchor { get; set; } public bool UsesFixedAnchor { get; set; }
[Resolved] [Resolved]
protected IGameplayClock GameplayClock { get; private set; } = null!; protected IGameplayClock GameplayClock { get; private set; } = null!;
[Resolved(canBeNull: true)] [Resolved]
private DrawableRuleset? drawableRuleset { get; set; } private IFrameStableClock? frameStableClock { get; set; }
/// <summary>
/// The reference clock is used to accurately tell the current playfield's time (including catch-up lag).
/// However, if none is available (i.e. used in tests), we fall back to the gameplay clock.
/// </summary>
protected IClock FrameStableClock => frameStableClock ?? GameplayClock;
private IClock? referenceClock;
private IEnumerable<HitObject>? objects; private IEnumerable<HitObject>? objects;
public IEnumerable<HitObject> Objects public IEnumerable<HitObject> Objects
@ -58,15 +74,21 @@ namespace osu.Game.Screens.Play.HUD
protected virtual void UpdateObjects(IEnumerable<HitObject> objects) { } protected virtual void UpdateObjects(IEnumerable<HitObject> objects) { }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(DrawableRuleset? drawableRuleset, Player? player)
{ {
if (drawableRuleset != null) if (drawableRuleset != null)
{ {
if (player?.Configuration.AllowUserInteraction == true)
((IBindable<bool>)Interactive).BindTo(drawableRuleset.HasReplayLoaded);
Objects = drawableRuleset.Objects; Objects = drawableRuleset.Objects;
referenceClock = drawableRuleset.FrameStableClock;
} }
} }
protected override void PopIn() => this.FadeIn(500, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(100);
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -74,9 +96,7 @@ namespace osu.Game.Screens.Play.HUD
if (objects == null) if (objects == null)
return; return;
// The reference clock is used to accurately tell the playfield's time. This is obtained from the drawable ruleset. double currentTime = FrameStableClock.CurrentTime;
// However, if no drawable ruleset is available (i.e. used in tests), we fall back to the gameplay clock.
double currentTime = referenceClock?.CurrentTime ?? GameplayClock.CurrentTime;
bool isInIntro = currentTime < FirstHitTime; bool isInIntro = currentTime < FirstHitTime;

View File

@ -10,6 +10,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using System; using System;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
@ -27,13 +28,33 @@ namespace osu.Game.Screens.Play.HUD
private double songLength => endTime - startTime; private double songLength => endTime - startTime;
private const int margin = 10; public FontUsage Font
{
set
{
timeCurrent.Font = value;
timeLeft.Font = value;
progress.Font = value;
}
}
public Colour4 TextColour
{
set
{
timeCurrent.Colour = value;
timeLeft.Colour = value;
progress.Colour = value;
}
}
public double StartTime public double StartTime
{ {
set => startTime = value; set => startTime = value;
} }
public bool ShowProgress { get; init; } = true;
public double EndTime public double EndTime
{ {
set => endTime = value; set => endTime = value;
@ -76,6 +97,7 @@ namespace osu.Game.Screens.Play.HUD
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Alpha = ShowProgress ? 1 : 0,
Child = new UprightAspectMaintainingContainer Child = new UprightAspectMaintainingContainer
{ {
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -99,15 +121,15 @@ namespace osu.Game.Screens.Play.HUD
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Child = new UprightAspectMaintainingContainer Child = new UprightAspectMaintainingContainer
{ {
Origin = Anchor.Centre, Origin = Anchor.CentreRight,
Anchor = Anchor.Centre, Anchor = Anchor.CentreRight,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Scaling = ScaleMode.Vertical, Scaling = ScaleMode.Vertical,
ScalingFactor = 0.5f, ScalingFactor = 0.5f,
Child = timeLeft = new SizePreservingSpriteText Child = timeLeft = new SizePreservingSpriteText
{ {
Origin = Anchor.Centre, Origin = Anchor.CentreRight,
Anchor = Anchor.Centre, Anchor = Anchor.CentreRight,
Colour = colours.BlueLighter, Colour = colours.BlueLighter,
Font = OsuFont.Numeric, Font = OsuFont.Numeric,
} }
@ -128,7 +150,7 @@ namespace osu.Game.Screens.Play.HUD
if (currentPercent != previousPercent) if (currentPercent != previousPercent)
{ {
progress.Text = currentPercent.ToString() + @"%"; progress.Text = currentPercent + @"%";
previousPercent = currentPercent; previousPercent = currentPercent;
} }

View File

@ -22,6 +22,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using osu.Game.Screens.Play.HUD.JudgementCounter;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osu.Game.Localisation; using osu.Game.Localisation;
@ -59,6 +60,9 @@ namespace osu.Game.Screens.Play
[Cached] [Cached]
private readonly ClicksPerSecondCalculator clicksPerSecondCalculator; private readonly ClicksPerSecondCalculator clicksPerSecondCalculator;
[Cached]
private readonly JudgementTally tally;
public Bindable<bool> ShowHealthBar = new Bindable<bool>(true); public Bindable<bool> ShowHealthBar = new Bindable<bool>(true);
private readonly DrawableRuleset drawableRuleset; private readonly DrawableRuleset drawableRuleset;
@ -104,6 +108,8 @@ namespace osu.Game.Screens.Play
Children = new Drawable[] Children = new Drawable[]
{ {
CreateFailingLayer(), CreateFailingLayer(),
//Needs to be initialized before skinnable drawables.
tally = new JudgementTally(),
mainComponents = new MainComponentsContainer mainComponents = new MainComponentsContainer
{ {
AlwaysPresent = true, AlwaysPresent = true,

View File

@ -309,6 +309,8 @@ namespace osu.Game.Screens.Play
}); });
} }
dependencies.CacheAs(DrawableRuleset.FrameStableClock);
// add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components. // add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
// also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.) // also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.)
// we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there. // we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.

View File

@ -61,50 +61,75 @@ namespace osu.Game.Screens.Select.Carousel
if (!(other is CarouselBeatmapSet otherSet)) if (!(other is CarouselBeatmapSet otherSet))
return base.CompareTo(criteria, other); return base.CompareTo(criteria, other);
int comparison = 0;
switch (criteria.Sort) switch (criteria.Sort)
{ {
default: default:
case SortMode.Artist: case SortMode.Artist:
return string.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist, StringComparison.OrdinalIgnoreCase); comparison = string.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist, StringComparison.OrdinalIgnoreCase);
break;
case SortMode.Title: case SortMode.Title:
return string.Compare(BeatmapSet.Metadata.Title, otherSet.BeatmapSet.Metadata.Title, StringComparison.OrdinalIgnoreCase); comparison = string.Compare(BeatmapSet.Metadata.Title, otherSet.BeatmapSet.Metadata.Title, StringComparison.OrdinalIgnoreCase);
break;
case SortMode.Author: case SortMode.Author:
return string.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username, StringComparison.OrdinalIgnoreCase); comparison = string.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username, StringComparison.OrdinalIgnoreCase);
break;
case SortMode.Source: case SortMode.Source:
return string.Compare(BeatmapSet.Metadata.Source, otherSet.BeatmapSet.Metadata.Source, StringComparison.OrdinalIgnoreCase); comparison = string.Compare(BeatmapSet.Metadata.Source, otherSet.BeatmapSet.Metadata.Source, StringComparison.OrdinalIgnoreCase);
break;
case SortMode.DateAdded: case SortMode.DateAdded:
return otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded); comparison = otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded);
break;
case SortMode.DateRanked: case SortMode.DateRanked:
// Beatmaps which have no ranked date should already be filtered away in this mode. // Beatmaps which have no ranked date should already be filtered away in this mode.
if (BeatmapSet.DateRanked == null || otherSet.BeatmapSet.DateRanked == null) if (BeatmapSet.DateRanked == null || otherSet.BeatmapSet.DateRanked == null)
return 0; break;
return otherSet.BeatmapSet.DateRanked.Value.CompareTo(BeatmapSet.DateRanked.Value); comparison = otherSet.BeatmapSet.DateRanked.Value.CompareTo(BeatmapSet.DateRanked.Value);
break;
case SortMode.LastPlayed: case SortMode.LastPlayed:
return -compareUsingAggregateMax(otherSet, b => (b.LastPlayed ?? DateTimeOffset.MinValue).ToUnixTimeSeconds()); comparison = -compareUsingAggregateMax(otherSet, b => (b.LastPlayed ?? DateTimeOffset.MinValue).ToUnixTimeSeconds());
break;
case SortMode.BPM: case SortMode.BPM:
return compareUsingAggregateMax(otherSet, b => b.BPM); comparison = compareUsingAggregateMax(otherSet, b => b.BPM);
break;
case SortMode.Length: case SortMode.Length:
return compareUsingAggregateMax(otherSet, b => b.Length); comparison = compareUsingAggregateMax(otherSet, b => b.Length);
break;
case SortMode.Difficulty: case SortMode.Difficulty:
return compareUsingAggregateMax(otherSet, b => b.StarRating); comparison = compareUsingAggregateMax(otherSet, b => b.StarRating);
break;
case SortMode.DateSubmitted: case SortMode.DateSubmitted:
// Beatmaps which have no submitted date should already be filtered away in this mode. // Beatmaps which have no submitted date should already be filtered away in this mode.
if (BeatmapSet.DateSubmitted == null || otherSet.BeatmapSet.DateSubmitted == null) if (BeatmapSet.DateSubmitted == null || otherSet.BeatmapSet.DateSubmitted == null)
return 0; break;
return otherSet.BeatmapSet.DateSubmitted.Value.CompareTo(BeatmapSet.DateSubmitted.Value); comparison = otherSet.BeatmapSet.DateSubmitted.Value.CompareTo(BeatmapSet.DateSubmitted.Value);
break;
} }
if (comparison != 0) return comparison;
// If the initial sort could not differentiate, attempt to use DateAdded to order sets in a stable fashion.
// The directionality of this matches the current SortMode.DateAdded, but we may want to reconsider if that becomes a user decision (ie. asc / desc).
comparison = otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded);
if (comparison != 0) return comparison;
// If DateAdded fails to break the tie, fallback to our internal GUID for stability.
// This basically means it's a stable random sort.
return otherSet.BeatmapSet.ID.CompareTo(BeatmapSet.ID);
} }
/// <summary> /// <summary>

View File

@ -108,6 +108,7 @@ namespace osu.Game.Skinning
var accuracy = container.OfType<DefaultAccuracyCounter>().FirstOrDefault(); var accuracy = container.OfType<DefaultAccuracyCounter>().FirstOrDefault();
var combo = container.OfType<DefaultComboCounter>().FirstOrDefault(); var combo = container.OfType<DefaultComboCounter>().FirstOrDefault();
var ppCounter = container.OfType<PerformancePointsCounter>().FirstOrDefault(); var ppCounter = container.OfType<PerformancePointsCounter>().FirstOrDefault();
var songProgress = container.OfType<ArgonSongProgress>().FirstOrDefault();
if (score != null) if (score != null)
{ {
@ -158,6 +159,12 @@ namespace osu.Game.Skinning
// origin flipped to match scale above. // origin flipped to match scale above.
hitError2.Origin = Anchor.CentreLeft; hitError2.Origin = Anchor.CentreLeft;
} }
if (songProgress != null)
{
songProgress.Position = new Vector2(0, -10);
songProgress.Scale = new Vector2(0.9f, 1);
}
} }
}) })
{ {
@ -167,7 +174,7 @@ namespace osu.Game.Skinning
new DefaultScoreCounter(), new DefaultScoreCounter(),
new DefaultAccuracyCounter(), new DefaultAccuracyCounter(),
new DefaultHealthDisplay(), new DefaultHealthDisplay(),
new DefaultSongProgress(), new ArgonSongProgress(),
new BarHitErrorMeter(), new BarHitErrorMeter(),
new BarHitErrorMeter(), new BarHitErrorMeter(),
new PerformancePointsCounter() new PerformancePointsCounter()

View File

@ -65,11 +65,14 @@ namespace osu.Game.Skinning
default: default:
this.ScaleTo(0.6f).Then() this.ScaleTo(0.6f).Then()
.ScaleTo(1.1f, fade_in_length * 0.8f).Then() .ScaleTo(1.1f, fade_in_length * 0.8f).Then() // t = 0.8
// this is actually correct to match stable; there were overlapping transforms. .Delay(fade_in_length * 0.2f) // t = 1.0
.ScaleTo(0.9f).Delay(fade_in_length * 0.2f) .ScaleTo(0.9f, fade_in_length * 0.2f).Then() // t = 1.2
.ScaleTo(1.1f).ScaleTo(0.9f, fade_in_length * 0.2f).Then()
.ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f); // stable dictates scale of 0.9->1 over time 1.0 to 1.4, but we are already at 1.2.
// so we need to force the current value to be correct at 1.2 (0.95) then complete the
// second half of the transform.
.ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f); // t = 1.4
break; break;
} }
} }

View File

@ -15,6 +15,10 @@ namespace osu.Game.Skinning
{ {
private CircularProgress circularProgress = null!; private CircularProgress circularProgress = null!;
// Legacy song progress doesn't support interaction for now.
public override bool HandleNonPositionalInput => false;
public override bool HandlePositionalInput => false;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -56,16 +60,6 @@ namespace osu.Game.Skinning
}; };
} }
protected override void PopIn()
{
this.FadeIn(500, Easing.OutQuint);
}
protected override void PopOut()
{
this.FadeOut(100);
}
protected override void UpdateProgress(double progress, bool isIntro) protected override void UpdateProgress(double progress, bool isIntro)
{ {
if (isIntro) if (isIntro)

View File

@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
BorderColour = Color4.White, BorderColour = Color4.White,
BorderThickness = 5, BorderThickness = 3,
Masking = true, Masking = true,
Children = new Drawable[] Children = new Drawable[]
@ -142,8 +142,15 @@ namespace osu.Game.Tests.Visual
c.AutoSizeAxes = Axes.None; c.AutoSizeAxes = Axes.None;
c.Size = Vector2.Zero; c.Size = Vector2.Zero;
c.RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None; if (autoSize)
c.AutoSizeAxes = autoSize ? Axes.Both : Axes.None; c.AutoSizeAxes = Axes.Both;
else
{
c.RelativeSizeAxes = Axes.Both;
c.Anchor = Anchor.Centre;
c.Origin = Anchor.Centre;
c.Size = new Vector2(0.97f);
}
} }
outlineBox.Alpha = autoSize ? 1 : 0; outlineBox.Alpha = autoSize ? 1 : 0;