1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 10:42:54 +08:00

Merge branch 'master' into editor/checks/delayed-hitsounds

This commit is contained in:
tsrk 2023-08-24 21:05:59 +02:00
commit 12bff9659b
No known key found for this signature in database
GPG Key ID: EBD46BB3049B56D6
22 changed files with 246 additions and 105 deletions

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.817.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2023.823.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -1,17 +1,19 @@
// 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 System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -19,9 +21,11 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
@ -30,8 +34,20 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public partial class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene public partial class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene
{ {
private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss private readonly OsuHitWindows referenceHitWindows;
private const double late_miss_window = 500; // time after +500 is considered a miss
/// <summary>
/// This is provided as a convenience for testing note lock behaviour against osu!stable.
/// Setting this field to a non-null path will cause beatmap files and replays used in all test cases
/// to be exported to disk so that they can be cross-checked against stable.
/// </summary>
private readonly string? exportLocation = null;
public TestSceneObjectOrderedHitPolicy()
{
referenceHitWindows = new OsuHitWindows();
referenceHitWindows.SetDifficulty(0);
}
/// <summary> /// <summary>
/// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged. /// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged.
@ -46,12 +62,12 @@ namespace osu.Game.Rulesets.Osu.Tests
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {
new TestHitCircle new HitCircle
{ {
StartTime = time_first_circle, StartTime = time_first_circle,
Position = positionFirstCircle Position = positionFirstCircle
}, },
new TestHitCircle new HitCircle
{ {
StartTime = time_second_circle, StartTime = time_second_circle,
Position = positionSecondCircle Position = positionSecondCircle
@ -65,7 +81,8 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementAssert(hitObjects[0], HitResult.Miss); addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[1], HitResult.Miss); addJudgementAssert(hitObjects[1], HitResult.Miss);
addJudgementOffsetAssert(hitObjects[0], late_miss_window); // note lock prevented the object from being hit, so the judgement offset should be very late.
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
} }
/// <summary> /// <summary>
@ -81,12 +98,12 @@ namespace osu.Game.Rulesets.Osu.Tests
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {
new TestHitCircle new HitCircle
{ {
StartTime = time_first_circle, StartTime = time_first_circle,
Position = positionFirstCircle Position = positionFirstCircle
}, },
new TestHitCircle new HitCircle
{ {
StartTime = time_second_circle, StartTime = time_second_circle,
Position = positionSecondCircle Position = positionSecondCircle
@ -100,7 +117,8 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementAssert(hitObjects[0], HitResult.Miss); addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[1], HitResult.Miss); addJudgementAssert(hitObjects[1], HitResult.Miss);
addJudgementOffsetAssert(hitObjects[0], late_miss_window); // note lock prevented the object from being hit, so the judgement offset should be very late.
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
} }
/// <summary> /// <summary>
@ -116,12 +134,12 @@ namespace osu.Game.Rulesets.Osu.Tests
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {
new TestHitCircle new HitCircle
{ {
StartTime = time_first_circle, StartTime = time_first_circle,
Position = positionFirstCircle Position = positionFirstCircle
}, },
new TestHitCircle new HitCircle
{ {
StartTime = time_second_circle, StartTime = time_second_circle,
Position = positionSecondCircle Position = positionSecondCircle
@ -135,7 +153,8 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementAssert(hitObjects[0], HitResult.Miss); addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[1], HitResult.Miss); addJudgementAssert(hitObjects[1], HitResult.Miss);
addJudgementOffsetAssert(hitObjects[0], late_miss_window); // note lock prevented the object from being hit, so the judgement offset should be very late.
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
} }
/// <summary> /// <summary>
@ -151,12 +170,12 @@ namespace osu.Game.Rulesets.Osu.Tests
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {
new TestHitCircle new HitCircle
{ {
StartTime = time_first_circle, StartTime = time_first_circle,
Position = positionFirstCircle Position = positionFirstCircle
}, },
new TestHitCircle new HitCircle
{ {
StartTime = time_second_circle, StartTime = time_second_circle,
Position = positionSecondCircle Position = positionSecondCircle
@ -165,14 +184,14 @@ namespace osu.Game.Rulesets.Osu.Tests
performTest(hitObjects, new List<ReplayFrame> performTest(hitObjects, new List<ReplayFrame>
{ {
new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_first_circle - 190, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } new OsuReplayFrame { Time = time_first_circle - 90, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
}); });
addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[0], HitResult.Meh);
addJudgementAssert(hitObjects[1], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Meh);
addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 addJudgementOffsetAssert(hitObjects[0], -90); // time_second_circle - first_circle_time - 90
} }
/// <summary> /// <summary>
@ -188,12 +207,12 @@ namespace osu.Game.Rulesets.Osu.Tests
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {
new TestHitCircle new HitCircle
{ {
StartTime = time_first_circle, StartTime = time_first_circle,
Position = positionFirstCircle Position = positionFirstCircle
}, },
new TestHitCircle new HitCircle
{ {
StartTime = time_second_circle, StartTime = time_second_circle,
Position = positionSecondCircle Position = positionSecondCircle
@ -202,13 +221,13 @@ namespace osu.Game.Rulesets.Osu.Tests
performTest(hitObjects, new List<ReplayFrame> performTest(hitObjects, new List<ReplayFrame>
{ {
new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_first_circle - 190, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
}); });
addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[0], HitResult.Meh);
addJudgementAssert(hitObjects[1], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Ok);
addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time
} }
@ -225,19 +244,19 @@ namespace osu.Game.Rulesets.Osu.Tests
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {
new TestHitCircle new HitCircle
{ {
StartTime = time_circle, StartTime = time_circle,
Position = positionCircle Position = positionCircle
}, },
new TestSlider new Slider
{ {
StartTime = time_slider, StartTime = time_slider,
Position = positionSlider, Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[] Path = new SliderPath(PathType.Linear, new[]
{ {
Vector2.Zero, Vector2.Zero,
new Vector2(25, 0), new Vector2(50, 0),
}) })
} }
}; };
@ -267,19 +286,19 @@ namespace osu.Game.Rulesets.Osu.Tests
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {
new TestHitCircle new HitCircle
{ {
StartTime = time_circle, StartTime = time_circle,
Position = positionCircle Position = positionCircle
}, },
new TestSlider new Slider
{ {
StartTime = time_slider, StartTime = time_slider,
Position = positionSlider, Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[] Path = new SliderPath(PathType.Linear, new[]
{ {
Vector2.Zero, Vector2.Zero,
new Vector2(25, 0), new Vector2(50, 0),
}) })
} }
}; };
@ -287,11 +306,11 @@ namespace osu.Game.Rulesets.Osu.Tests
performTest(hitObjects, new List<ReplayFrame> performTest(hitObjects, new List<ReplayFrame>
{ {
new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_circle + referenceHitWindows.WindowFor(HitResult.Meh) - 100, Position = positionCircle, Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_circle + referenceHitWindows.WindowFor(HitResult.Meh) - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } },
}); });
addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[0], HitResult.Ok);
addJudgementAssert(hitObjects[1], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit); addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
@ -304,7 +323,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public void TestHitCircleBeforeSpinner() public void TestHitCircleBeforeSpinner()
{ {
const double time_spinner = 1500; const double time_spinner = 1500;
const double time_circle = 1800; const double time_circle = 1600;
Vector2 positionCircle = Vector2.Zero; Vector2 positionCircle = Vector2.Zero;
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
@ -315,7 +334,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(256, 192), Position = new Vector2(256, 192),
EndTime = time_spinner + 1000, EndTime = time_spinner + 1000,
}, },
new TestHitCircle new HitCircle
{ {
StartTime = time_circle, StartTime = time_circle,
Position = positionCircle Position = positionCircle
@ -324,7 +343,7 @@ namespace osu.Game.Rulesets.Osu.Tests
performTest(hitObjects, new List<ReplayFrame> performTest(hitObjects, new List<ReplayFrame>
{ {
new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_spinner - 90, Position = positionCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } },
@ -333,7 +352,7 @@ namespace osu.Game.Rulesets.Osu.Tests
}); });
addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Meh);
} }
[Test] [Test]
@ -346,12 +365,12 @@ namespace osu.Game.Rulesets.Osu.Tests
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {
new TestHitCircle new HitCircle
{ {
StartTime = time_circle, StartTime = time_circle,
Position = positionCircle Position = positionCircle
}, },
new TestSlider new Slider
{ {
StartTime = time_slider, StartTime = time_slider,
Position = positionSlider, Position = positionSlider,
@ -380,38 +399,105 @@ namespace osu.Game.Rulesets.Osu.Tests
() => judgementResults.Single(r => r.HitObject == hitObject).Type, () => Is.EqualTo(result)); () => judgementResults.Single(r => r.HitObject == hitObject).Type, () => Is.EqualTo(result));
} }
private void addJudgementAssert(string name, Func<OsuHitObject> hitObject, HitResult result) private void addJudgementAssert(string name, Func<OsuHitObject?> hitObject, HitResult result)
{ {
AddAssert($"{name} judgement is {result}", AddAssert($"{name} judgement is {result}",
() => judgementResults.Single(r => r.HitObject == hitObject()).Type == result); () => judgementResults.Single(r => r.HitObject == hitObject()).Type, () => Is.EqualTo(result));
} }
private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset) private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
{ {
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
() => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100)); () => judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, () => Is.EqualTo(offset).Within(100));
} }
private ScoreAccessibleReplayPlayer currentPlayer; private ScoreAccessibleReplayPlayer currentPlayer = null!;
private List<JudgementResult> judgementResults; private List<JudgementResult> judgementResults = null!;
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames) private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames, [CallerMemberName] string testCaseName = "")
{ {
AddStep("load player", () => IBeatmap playableBeatmap = null!;
Score score = null!;
AddStep("create beatmap", () =>
{ {
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject> Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
{ {
Metadata =
{
Title = testCaseName
},
HitObjects = hitObjects, HitObjects = hitObjects,
Difficulty = new BeatmapDifficulty { SliderTickRate = 3 }, Difficulty = new BeatmapDifficulty
{
OverallDifficulty = 0,
SliderTickRate = 3
},
BeatmapInfo = BeatmapInfo =
{ {
Ruleset = new OsuRuleset().RulesetInfo Ruleset = new OsuRuleset().RulesetInfo,
BeatmapVersion = LegacyBeatmapEncoder.FIRST_LAZER_VERSION // for correct offset treatment by score encoder
}, },
ControlPointInfo = cpi
});
playableBeatmap = Beatmap.Value.GetPlayableBeatmap(new OsuRuleset().RulesetInfo);
});
AddStep("create score", () =>
{
score = new Score
{
Replay = new Replay
{
Frames = new List<ReplayFrame>
{
// required for correct playback in stable
new OsuReplayFrame(0, new Vector2(256, -500)),
new OsuReplayFrame(0, new Vector2(256, -500))
}.Concat(frames).ToList()
},
ScoreInfo =
{
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = playableBeatmap.BeatmapInfo
}
};
});
if (exportLocation != null)
{
AddStep("export beatmap", () =>
{
var beatmapEncoder = new LegacyBeatmapEncoder(playableBeatmap, null);
using (var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osu"), FileMode.Create))
{
var memoryStream = new MemoryStream();
using (var writer = new StreamWriter(memoryStream, Encoding.UTF8, leaveOpen: true))
beatmapEncoder.Encode(writer);
memoryStream.Seek(0, SeekOrigin.Begin);
memoryStream.CopyTo(stream);
memoryStream.Seek(0, SeekOrigin.Begin);
playableBeatmap.BeatmapInfo.MD5Hash = memoryStream.ComputeMD5Hash();
}
}); });
AddStep("export score", () =>
{
using var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osr"), FileMode.Create);
var encoder = new LegacyScoreEncoder(score, playableBeatmap);
encoder.Encode(stream);
});
}
AddStep("load player", () =>
{
SelectedMods.Value = new[] { new OsuModClassic() }; SelectedMods.Value = new[] { new OsuModClassic() };
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); var p = new ScoreAccessibleReplayPlayer(score);
p.OnLoadComplete += _ => p.OnLoadComplete += _ =>
{ {
@ -430,28 +516,6 @@ namespace osu.Game.Rulesets.Osu.Tests
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
} }
private class TestHitCircle : HitCircle
{
protected override HitWindows CreateHitWindows() => new TestHitWindows();
}
private class TestSlider : Slider
{
public TestSlider()
{
SliderVelocity = 0.1f;
DefaultsApplied += _ =>
{
HeadCircle.HitWindows = new TestHitWindows();
TailCircle.HitWindows = new TestHitWindows();
HeadCircle.HitWindows.SetDifficulty(0);
TailCircle.HitWindows.SetDifficulty(0);
};
}
}
private class TestSpinner : Spinner private class TestSpinner : Spinner
{ {
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
@ -461,19 +525,6 @@ namespace osu.Game.Rulesets.Osu.Tests
} }
} }
private class TestHitWindows : HitWindows
{
private static readonly DifficultyRange[] ranges =
{
new DifficultyRange(HitResult.Great, 500, 500, 500),
new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window),
};
public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss;
protected override DifficultyRange[] GetRanges() => ranges;
}
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
{ {
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;

View File

@ -23,6 +23,9 @@ namespace osu.Game.Collections
private AudioFilter lowPassFilter = null!; private AudioFilter lowPassFilter = null!;
protected override string PopInSampleName => @"UI/overlay-big-pop-in";
protected override string PopOutSampleName => @"UI/overlay-big-pop-out";
public ManageCollectionsDialog() public ManageCollectionsDialog()
{ {
Anchor = Anchor.Centre; Anchor = Anchor.Centre;

View File

@ -24,6 +24,7 @@ namespace osu.Game.Graphics.Containers
private Sample samplePopOut; private Sample samplePopOut;
protected virtual string PopInSampleName => "UI/overlay-pop-in"; protected virtual string PopInSampleName => "UI/overlay-pop-in";
protected virtual string PopOutSampleName => "UI/overlay-pop-out"; protected virtual string PopOutSampleName => "UI/overlay-pop-out";
protected virtual double PopInOutSampleBalance => 0;
protected override bool BlockNonPositionalInput => true; protected override bool BlockNonPositionalInput => true;
@ -133,15 +134,21 @@ namespace osu.Game.Graphics.Containers
return; return;
} }
if (didChange) if (didChange && samplePopIn != null)
samplePopIn?.Play(); {
samplePopIn.Balance.Value = PopInOutSampleBalance;
samplePopIn.Play();
}
if (BlockScreenWideMouse && DimMainContent) overlayManager?.ShowBlockingOverlay(this); if (BlockScreenWideMouse && DimMainContent) overlayManager?.ShowBlockingOverlay(this);
break; break;
case Visibility.Hidden: case Visibility.Hidden:
if (didChange) if (didChange && samplePopOut != null)
samplePopOut?.Play(); {
samplePopOut.Balance.Value = PopInOutSampleBalance;
samplePopOut.Play();
}
if (BlockScreenWideMouse) overlayManager?.HideBlockingOverlay(this); if (BlockScreenWideMouse) overlayManager?.HideBlockingOverlay(this);
break; break;

View File

@ -2,6 +2,9 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
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;
@ -32,6 +35,12 @@ namespace osu.Game.Graphics.Containers
protected override bool StartHidden => true; protected override bool StartHidden => true;
private Sample? samplePopIn;
private Sample? samplePopOut;
// required due to LoadAsyncComplete() in `VisibilityContainer` calling PopOut() during load - similar workaround to `OsuDropdownMenu`
private bool wasShown;
public Color4 FirstWaveColour public Color4 FirstWaveColour
{ {
get => firstWave.Colour; get => firstWave.Colour;
@ -56,6 +65,13 @@ namespace osu.Game.Graphics.Containers
set => fourthWave.Colour = value; set => fourthWave.Colour = value;
} }
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio)
{
samplePopIn = audio.Samples.Get("UI/wave-pop-in");
samplePopOut = audio.Samples.Get("UI/overlay-big-pop-out");
}
public WaveContainer() public WaveContainer()
{ {
Masking = true; Masking = true;
@ -110,6 +126,8 @@ namespace osu.Game.Graphics.Containers
w.Show(); w.Show();
contentContainer.MoveToY(0, APPEAR_DURATION, Easing.OutQuint); contentContainer.MoveToY(0, APPEAR_DURATION, Easing.OutQuint);
samplePopIn?.Play();
wasShown = true;
} }
protected override void PopOut() protected override void PopOut()
@ -118,6 +136,9 @@ namespace osu.Game.Graphics.Containers
w.Hide(); w.Hide();
contentContainer.MoveToY(2, DISAPPEAR_DURATION, Easing.In); contentContainer.MoveToY(2, DISAPPEAR_DURATION, Easing.In);
if (wasShown)
samplePopOut?.Play();
} }
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()

View File

@ -46,8 +46,8 @@ namespace osu.Game.Graphics.UserInterface
private readonly Container content; private readonly Container content;
private readonly Box hover; private readonly Box hover;
public OsuAnimatedButton() public OsuAnimatedButton(HoverSampleSet sampleSet = HoverSampleSet.Button)
: base(HoverSampleSet.Button) : base(sampleSet)
{ {
base.Content.Add(content = new Container base.Content.Add(content = new Container
{ {

View File

@ -14,6 +14,12 @@ namespace osu.Game.Graphics.UserInterface
private Sample? sampleOff; private Sample? sampleOff;
private Sample? sampleOn; private Sample? sampleOn;
/// <summary>
/// Sheared toggle buttons by default play two samples when toggled: a click and a toggle (on/off).
/// Sometimes this might be too much. Setting this to <c>false</c> will silence the toggle sound.
/// </summary>
protected virtual bool PlayToggleSamples => true;
/// <summary> /// <summary>
/// Whether this button is currently toggled to an active state. /// Whether this button is currently toggled to an active state.
/// </summary> /// </summary>
@ -68,10 +74,13 @@ namespace osu.Game.Graphics.UserInterface
{ {
sampleClick?.Play(); sampleClick?.Play();
if (Active.Value) if (PlayToggleSamples)
sampleOn?.Play(); {
else if (Active.Value)
sampleOff?.Play(); sampleOn?.Play();
else
sampleOff?.Play();
}
} }
} }
} }

View File

@ -2,6 +2,8 @@
// 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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -21,6 +23,14 @@ namespace osu.Game.Graphics.UserInterfaceV2
private const float fade_duration = 250; private const float fade_duration = 250;
private const double scale_duration = 500; private const double scale_duration = 500;
private Sample? samplePopIn;
private Sample? samplePopOut;
protected virtual string PopInSampleName => "UI/overlay-pop-in";
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
// required due to LoadAsyncComplete() in `VisibilityContainer` calling PopOut() during load - similar workaround to `OsuDropdownMenu`
private bool wasOpened;
public OsuPopover(bool withPadding = true) public OsuPopover(bool withPadding = true)
{ {
Content.Padding = withPadding ? new MarginPadding(20) : new MarginPadding(); Content.Padding = withPadding ? new MarginPadding(20) : new MarginPadding();
@ -38,9 +48,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider? colourProvider, OsuColour colours) private void load(OverlayColourProvider? colourProvider, OsuColour colours, AudioManager audio)
{ {
Background.Colour = Arrow.Colour = colourProvider?.Background4 ?? colours.GreySeaFoamDarker; Background.Colour = Arrow.Colour = colourProvider?.Background4 ?? colours.GreySeaFoamDarker;
samplePopIn = audio.Samples.Get(PopInSampleName);
samplePopOut = audio.Samples.Get(PopOutSampleName);
} }
protected override Drawable CreateArrow() => Empty(); protected override Drawable CreateArrow() => Empty();
@ -49,12 +61,18 @@ namespace osu.Game.Graphics.UserInterfaceV2
{ {
this.ScaleTo(1, scale_duration, Easing.OutElasticHalf); this.ScaleTo(1, scale_duration, Easing.OutElasticHalf);
this.FadeIn(fade_duration, Easing.OutQuint); this.FadeIn(fade_duration, Easing.OutQuint);
samplePopIn?.Play();
wasOpened = true;
} }
protected override void PopOut() protected override void PopOut()
{ {
this.ScaleTo(0.7f, scale_duration, Easing.OutQuint); this.ScaleTo(0.7f, scale_duration, Easing.OutQuint);
this.FadeOut(fade_duration, Easing.OutQuint); this.FadeOut(fade_duration, Easing.OutQuint);
if (wasOpened)
samplePopOut?.Play();
} }
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)

View File

@ -55,6 +55,9 @@ namespace osu.Game.Overlays
private const float side_bar_width = 190; private const float side_bar_width = 190;
private const float chat_bar_height = 60; private const float chat_bar_height = 60;
protected override string PopInSampleName => @"UI/overlay-big-pop-in";
protected override string PopOutSampleName => @"UI/overlay-big-pop-out";
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } = null!; private OsuConfigManager config { get; set; } = null!;

View File

@ -20,6 +20,8 @@ namespace osu.Game.Overlays
private const float transition_time = 400; private const float transition_time = 400;
protected override double PopInOutSampleBalance => OsuGameBase.SFX_STEREO_STRENGTH;
[Cached] [Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);

View File

@ -18,6 +18,8 @@ namespace osu.Game.Overlays.Mods
{ {
public partial class AddPresetButton : ShearedToggleButton, IHasPopover public partial class AddPresetButton : ShearedToggleButton, IHasPopover
{ {
protected override bool PlayToggleSamples => false;
[Resolved] [Resolved]
private OsuColour colours { get; set; } = null!; private OsuColour colours { get; set; } = null!;

View File

@ -31,6 +31,8 @@ namespace osu.Game.Overlays
public LocalisableString Title => NotificationsStrings.HeaderTitle; public LocalisableString Title => NotificationsStrings.HeaderTitle;
public LocalisableString Description => NotificationsStrings.HeaderDescription; public LocalisableString Description => NotificationsStrings.HeaderDescription;
protected override double PopInOutSampleBalance => OsuGameBase.SFX_STEREO_STRENGTH;
public const float WIDTH = 320; public const float WIDTH = 320;
public const float TRANSITION_LENGTH = 600; public const float TRANSITION_LENGTH = 600;

View File

@ -56,6 +56,7 @@ namespace osu.Game.Overlays
private SeekLimitedSearchTextBox searchTextBox; private SeekLimitedSearchTextBox searchTextBox;
protected override string PopInSampleName => "UI/settings-pop-in"; protected override string PopInSampleName => "UI/settings-pop-in";
protected override double PopInOutSampleBalance => -OsuGameBase.SFX_STEREO_STRENGTH;
private readonly bool showSidebar; private readonly bool showSidebar;

View File

@ -18,7 +18,9 @@ namespace osu.Game.Overlays
protected override bool StartHidden => true; protected override bool StartHidden => true;
protected override string PopInSampleName => "UI/wave-pop-in"; // `WaveContainer` plays PopIn/PopOut samples, so we disable the overlay-level one as to not double-up sample playback.
protected override string PopInSampleName => string.Empty;
protected override string PopOutSampleName => string.Empty;
public const float HORIZONTAL_PADDING = 50; public const float HORIZONTAL_PADDING = 50;

View File

@ -71,8 +71,21 @@ namespace osu.Game.Rulesets.Mods
{ {
var bindable = (IBindable)property.GetValue(this)!; var bindable = (IBindable)property.GetValue(this)!;
string valueText;
switch (bindable)
{
case Bindable<bool> b:
valueText = b.Value ? "on" : "off";
break;
default:
valueText = bindable.ToString() ?? string.Empty;
break;
}
if (!bindable.IsDefault) if (!bindable.IsDefault)
tooltipTexts.Add($"{attr.Label} {bindable}"); tooltipTexts.Add($"{attr.Label}: {valueText}");
} }
return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s))); return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s)));

View File

@ -262,6 +262,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private readonly OsuSpriteText divisorText; private readonly OsuSpriteText divisorText;
public DivisorDisplay() public DivisorDisplay()
: base(HoverSampleSet.Default)
{ {
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;

View File

@ -114,6 +114,9 @@ namespace osu.Game.Screens.Edit.Setup
private partial class FileChooserPopover : OsuPopover private partial class FileChooserPopover : OsuPopover
{ {
protected override string PopInSampleName => "UI/overlay-big-pop-in";
protected override string PopOutSampleName => "UI/overlay-big-pop-out";
public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> currentFile, string? chooserPath) public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> currentFile, string? chooserPath)
{ {
Child = new Container Child = new Container

View File

@ -170,7 +170,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
if (Room.HasPassword.Value) if (Room.HasPassword.Value)
{ {
sampleJoin?.Play();
this.ShowPopover(); this.ShowPopover();
return true; return true;
} }

View File

@ -32,6 +32,9 @@ namespace osu.Game.Screens.Select.Options
public override bool BlockScreenWideMouse => false; public override bool BlockScreenWideMouse => false;
protected override string PopInSampleName => "SongSelect/options-pop-in";
protected override string PopOutSampleName => "SongSelect/options-pop-out";
public BeatmapOptionsOverlay() public BeatmapOptionsOverlay()
{ {
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;

View File

@ -47,6 +47,7 @@ namespace osu.Game.Utils
options.AutoSessionTracking = true; options.AutoSessionTracking = true;
options.IsEnvironmentUser = false; options.IsEnvironmentUser = false;
options.IsGlobalModeEnabled = true;
// The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml // The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml
options.Release = $"osu@{game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty)}"; options.Release = $"osu@{game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty)}";
}); });

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="11.1.2" /> <PackageReference Include="Realm" Version="11.1.2" />
<PackageReference Include="ppy.osu.Framework" Version="2023.822.0" /> <PackageReference Include="ppy.osu.Framework" Version="2023.823.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.822.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2023.822.0" />
<PackageReference Include="Sentry" Version="3.28.1" /> <PackageReference Include="Sentry" Version="3.28.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" /> <PackageReference Include="SharpCompress" Version="0.32.2" />

View File

@ -23,6 +23,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier> <RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.817.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2023.823.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>