1
0
mirror of https://github.com/ppy/osu synced 2025-03-22 19:06:58 +00:00

Merge branch 'master' into map_info_on_mod_settings

This commit is contained in:
Dean Herbert 2023-09-12 17:16:47 +09:00 committed by GitHub
commit 0d7157f727
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 175 additions and 15 deletions
osu.Game.Rulesets.Osu.Tests
osu.Game.Rulesets.Osu
osu.Game.Tests/Visual/Online
osu.Game/Overlays/Profile/Header

View File

@ -506,7 +506,6 @@ namespace osu.Game.Rulesets.Osu.Tests
} }
[Test] [Test]
[Ignore("Currently broken, first attempt at fixing broke even harder. See https://github.com/ppy/osu/issues/24743.")]
public void TestInputDoesNotFallThroughOverlappingSliders() public void TestInputDoesNotFallThroughOverlappingSliders()
{ {
const double time_first_slider = 1000; const double time_first_slider = 1000;
@ -550,12 +549,103 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0); addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
addJudgementAssert(hitObjects[1], HitResult.Miss); addJudgementAssert(hitObjects[1], HitResult.Miss);
// the slider head of the first slider prevents the second slider's head from being hit, so the judgement offset should be very late. // the slider head of the first slider prevents the second slider's head from being hit, so the judgement offset should be very late.
// this is not strictly done by the hit policy implementation itself (see `OsuModClassic.blockInputToObjectsUnderSliderHead()`),
// but we're testing this here anyways to just keep everything related to input handling and note lock in one place.
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, referenceHitWindows.WindowFor(HitResult.Meh)); addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, referenceHitWindows.WindowFor(HitResult.Meh));
addClickActionAssert(0, ClickAction.Hit); addClickActionAssert(0, ClickAction.Hit);
} }
[Test] [Test]
public void TestOverlappingObjectsDontBlockEachOtherWhenFullyFadedOut() public void TestOverlappingSlidersDontBlockEachOtherWhenFullyJudged()
{
const double time_first_slider = 1000;
const double time_second_slider = 1600;
Vector2 positionFirstSlider = new Vector2(100, 50);
Vector2 positionSecondSlider = new Vector2(100, 80);
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
var hitObjects = new List<OsuHitObject>
{
new Slider
{
StartTime = time_first_slider,
Position = positionFirstSlider,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(25, 0),
})
},
new Slider
{
StartTime = time_second_slider,
Position = positionSecondSlider,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(25, 0),
})
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint },
// this frame doesn't do anything on lazer, but is REQUIRED for correct playback on stable,
// because stable during replay playback only updates game state _when it encounters a replay frame_
new OsuReplayFrame { Time = 1250, Position = midpoint },
new OsuReplayFrame { Time = time_second_slider + 50, Position = midpoint, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_second_slider + 75, Position = midpoint },
});
addJudgementAssert(hitObjects[0], HitResult.Ok);
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
addJudgementAssert(hitObjects[1], HitResult.Ok);
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, 50);
addClickActionAssert(0, ClickAction.Hit);
addClickActionAssert(1, ClickAction.Hit);
}
[Test]
public void TestOverlappingHitCirclesDontBlockEachOtherWhenBothVisible()
{
const double time_first_circle = 1000;
const double time_second_circle = 1200;
Vector2 positionFirstCircle = new Vector2(100);
Vector2 positionSecondCircle = new Vector2(120);
var midpoint = (positionFirstCircle + positionSecondCircle) / 2;
var hitObjects = new List<OsuHitObject>
{
new HitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle,
},
new HitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle,
},
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle, Position = midpoint, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_first_circle + 25, Position = midpoint },
new OsuReplayFrame { Time = time_first_circle + 50, Position = midpoint, Actions = { OsuAction.RightButton } },
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementOffsetAssert(hitObjects[0], 0);
addJudgementAssert(hitObjects[1], HitResult.Meh);
addJudgementOffsetAssert(hitObjects[1], -150);
}
[Test]
public void TestOverlappingHitCirclesDontBlockEachOtherWhenFullyFadedOut()
{ {
const double time_first_circle = 1000; const double time_first_circle = 1000;
const double time_second_circle = 1200; const double time_second_circle = 1200;
@ -586,8 +676,10 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
new OsuReplayFrame { Time = time_first_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_first_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_first_circle + 50, Position = positionFirstCircle }, new OsuReplayFrame { Time = time_first_circle + 50, Position = positionFirstCircle },
new OsuReplayFrame { Time = time_second_circle - 50, Position = positionSecondCircle },
new OsuReplayFrame { Time = time_second_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_second_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_second_circle + 50, Position = positionSecondCircle }, new OsuReplayFrame { Time = time_second_circle + 50, Position = positionSecondCircle },
new OsuReplayFrame { Time = time_third_circle - 50, Position = positionFirstCircle },
new OsuReplayFrame { Time = time_third_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_third_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_third_circle + 50, Position = positionFirstCircle }, new OsuReplayFrame { Time = time_third_circle + 50, Position = positionFirstCircle },
}); });

View File

@ -74,6 +74,10 @@ namespace osu.Game.Rulesets.Osu.Mods
head.TrackFollowCircle = !NoSliderHeadMovement.Value; head.TrackFollowCircle = !NoSliderHeadMovement.Value;
if (FadeHitCircleEarly.Value && !usingHiddenFading) if (FadeHitCircleEarly.Value && !usingHiddenFading)
applyEarlyFading(head); applyEarlyFading(head);
if (ClassicNoteLock.Value)
blockInputToObjectsUnderSliderHead(head);
break; break;
case DrawableSliderTail tail: case DrawableSliderTail tail:
@ -83,10 +87,27 @@ namespace osu.Game.Rulesets.Osu.Mods
case DrawableHitCircle circle: case DrawableHitCircle circle:
if (FadeHitCircleEarly.Value && !usingHiddenFading) if (FadeHitCircleEarly.Value && !usingHiddenFading)
applyEarlyFading(circle); applyEarlyFading(circle);
break; break;
} }
} }
/// <summary>
/// On stable, slider heads that have already been hit block input from reaching objects that may be underneath them
/// until the sliders they're part of have been fully judged.
/// The purpose of this method is to restore that behaviour.
/// In order to avoid introducing yet another confusing config option, this behaviour is roped into the general notion of "note lock".
/// </summary>
private static void blockInputToObjectsUnderSliderHead(DrawableSliderHead slider)
{
var oldHitAction = slider.HitArea.Hit;
slider.HitArea.Hit = () =>
{
oldHitAction?.Invoke();
return !slider.DrawableSlider.AllJudged;
};
}
private void applyEarlyFading(DrawableHitCircle circle) private void applyEarlyFading(DrawableHitCircle circle)
{ {
circle.ApplyCustomUpdateState += (dho, state) => circle.ApplyCustomUpdateState += (dho, state) =>

View File

@ -261,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
case OsuAction.RightButton: case OsuAction.RightButton:
if (IsHovered && (Hit?.Invoke() ?? false)) if (IsHovered && (Hit?.Invoke() ?? false))
{ {
HitAction = e.Action; HitAction ??= e.Action;
return true; return true;
} }

View File

@ -110,5 +110,31 @@ namespace osu.Game.Tests.Visual.Online
} }
}, new OsuRuleset().RulesetInfo)); }, new OsuRuleset().RulesetInfo));
} }
[Test]
public void TestPreviousUsernames()
{
AddStep("Show user w/ previous usernames", () => header.User.Value = new UserProfileData(new APIUser
{
Id = 727,
Username = "SomeoneIndecisive",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg",
Groups = new[]
{
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
},
Statistics = new UserStatistics
{
IsRanked = false,
// web will sometimes return non-empty rank history even for unranked users.
RankHistory = new APIRankHistory
{
Mode = @"osu",
Data = Enumerable.Range(2345, 85).ToArray()
},
},
PreviousUsernames = new[] { "tsrk.", "quoicoubeh", "apagnan", "epita" }
}, new OsuRuleset().RulesetInfo));
}
} }
} }

View File

@ -5,20 +5,29 @@ using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Overlays.Profile.Header.Components;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
{ {
[TestFixture] [TestFixture]
public partial class TestSceneUserProfilePreviousUsernames : OsuTestScene public partial class TestSceneUserProfilePreviousUsernamesDisplay : OsuTestScene
{ {
private PreviousUsernames container = null!; private PreviousUsernamesDisplay container = null!;
private OverlayColourProvider colourProvider = null!;
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
{ {
Child = container = new PreviousUsernames colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
Child = new DependencyProvidingContainer
{ {
Child = container = new PreviousUsernamesDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
CachedDependencies = new (Type, object)[] { (typeof(OverlayColourProvider), colourProvider) },
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}; };

View File

@ -18,12 +18,13 @@ using osuTK;
namespace osu.Game.Overlays.Profile.Header.Components namespace osu.Game.Overlays.Profile.Header.Components
{ {
public partial class PreviousUsernames : CompositeDrawable public partial class PreviousUsernamesDisplay : CompositeDrawable
{ {
private const int duration = 200; private const int duration = 200;
private const int margin = 10; private const int margin = 10;
private const int width = 310; private const int width = 300;
private const int move_offset = 15; private const int move_offset = 15;
private const int base_y_offset = -3; // eye balled to make it look good
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>(); public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
@ -31,14 +32,15 @@ namespace osu.Game.Overlays.Profile.Header.Components
private readonly Box background; private readonly Box background;
private readonly SpriteText header; private readonly SpriteText header;
public PreviousUsernames() public PreviousUsernamesDisplay()
{ {
HoverIconContainer hoverIcon; HoverIconContainer hoverIcon;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Width = width; Width = width;
Masking = true; Masking = true;
CornerRadius = 5; CornerRadius = 6;
Y = base_y_offset;
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
@ -84,6 +86,9 @@ namespace osu.Game.Overlays.Profile.Header.Components
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full, Direction = FillDirection.Full,
// Prevents the tooltip of having a sudden size reduction and flickering when the text is being faded out.
// Also prevents a potential OnHover/HoverLost feedback loop.
AlwaysPresent = true,
Margin = new MarginPadding { Bottom = margin, Top = margin / 2f } Margin = new MarginPadding { Bottom = margin, Top = margin / 2f }
} }
} }
@ -96,9 +101,9 @@ namespace osu.Game.Overlays.Profile.Header.Components
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OverlayColourProvider colours)
{ {
background.Colour = colours.GreySeaFoamDarker; background.Colour = colours.Background6;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -134,7 +139,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
text.FadeIn(duration, Easing.OutQuint); text.FadeIn(duration, Easing.OutQuint);
header.FadeIn(duration, Easing.OutQuint); header.FadeIn(duration, Easing.OutQuint);
background.FadeIn(duration, Easing.OutQuint); background.FadeIn(duration, Easing.OutQuint);
this.MoveToY(-move_offset, duration, Easing.OutQuint); this.MoveToY(base_y_offset - move_offset, duration, Easing.OutQuint);
} }
private void hideContent() private void hideContent()
@ -142,7 +147,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
text.FadeOut(duration, Easing.OutQuint); text.FadeOut(duration, Easing.OutQuint);
header.FadeOut(duration, Easing.OutQuint); header.FadeOut(duration, Easing.OutQuint);
background.FadeOut(duration, Easing.OutQuint); background.FadeOut(duration, Easing.OutQuint);
this.MoveToY(0, duration, Easing.OutQuint); this.MoveToY(base_y_offset, duration, Easing.OutQuint);
} }
private partial class HoverIconContainer : Container private partial class HoverIconContainer : Container
@ -156,7 +161,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{ {
Margin = new MarginPadding { Top = 6, Left = margin, Right = margin * 2 }, Margin = new MarginPadding { Top = 6, Left = margin, Right = margin * 2 },
Size = new Vector2(15), Size = new Vector2(15),
Icon = FontAwesome.Solid.IdCard, Icon = FontAwesome.Solid.AddressCard,
}; };
} }

View File

@ -46,6 +46,7 @@ namespace osu.Game.Overlays.Profile.Header
private OsuSpriteText userCountryText = null!; private OsuSpriteText userCountryText = null!;
private GroupBadgeFlow groupBadgeFlow = null!; private GroupBadgeFlow groupBadgeFlow = null!;
private ToggleCoverButton coverToggle = null!; private ToggleCoverButton coverToggle = null!;
private PreviousUsernamesDisplay previousUsernamesDisplay = null!;
private Bindable<bool> coverExpanded = null!; private Bindable<bool> coverExpanded = null!;
@ -143,6 +144,11 @@ namespace osu.Game.Overlays.Profile.Header
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
}, },
new Container
{
// Intentionally use a zero-size container, else the fill flow will adjust to (and cancel) the upwards animation.
Child = previousUsernamesDisplay = new PreviousUsernamesDisplay(),
}
} }
}, },
titleText = new OsuSpriteText titleText = new OsuSpriteText
@ -216,6 +222,7 @@ namespace osu.Game.Overlays.Profile.Header
titleText.Text = user?.Title ?? string.Empty; titleText.Text = user?.Title ?? string.Empty;
titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff"); titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff");
groupBadgeFlow.User.Value = user; groupBadgeFlow.User.Value = user;
previousUsernamesDisplay.User.Value = user;
} }
private void updateCoverState() private void updateCoverState()