22 Commits

Author SHA1 Message Date
628181a883 also update colour on load 2025-08-27 01:11:57 +03:00
835329efd3 synchronize with github 2025-08-27 00:59:45 +03:00
Dean Herbert
2bea59e65f Merge pull request #34802 from bdach/hack-around-carousel-panel-refresh
Work around excessive refreshes of carousel beatmap set panel backgrounds
2025-08-26 21:13:28 +09:00
Bartłomiej Dach
c0fd5637de Work around excessive refreshes of carousel beatmap set panel backgrounds
Closes https://github.com/ppy/osu/issues/34511 I guess.
2025-08-26 13:27:54 +02:00
Bartłomiej Dach
5e7a99c97f Merge pull request #34801 from peppy/replay-player-null
Fix crash on exiting `ReplayPlayer` is beatmap was not loaded successfully
2025-08-26 12:11:27 +02:00
Bartłomiej Dach
8f628d16ae Merge pull request #34800 from peppy/fix-daily-challenge-leaderboard-skip
Fix daily challenge / playlist leaderboard sometimes showing incorrect default state
2025-08-26 12:07:41 +02:00
Dean Herbert
2ccb65aa65 Add test coverage and fix one more fail case 2025-08-26 18:41:14 +09:00
Dean Herbert
4d851f2527 Fix crash on exiting ReplayPlayer is beatmap was not loaded successfully
Closes https://github.com/ppy/osu/issues/34763.
2025-08-26 18:31:42 +09:00
Dean Herbert
4bafbfb9e4 Apply NRT to ReplayPlayer for good measure 2025-08-26 18:30:12 +09:00
Dean Herbert
3f179e3903 Sort scores immediately for good measure 2025-08-26 17:51:14 +09:00
Dean Herbert
196b28115e Fix playlist leaderboard provider potentially inserting local user in wrong order
Due to `Perform` being used from a BDL method in conjunction with
`Success` (which is scheduled to the *update* thread), there was a
chance that the order of execution would be not quite as intended.

To rectify, let's not use `Success` and just continue with synchronous
flow.
2025-08-26 17:51:02 +09:00
Dean Herbert
7660a9ba8e Merge pull request #34794 from bdach/fix-aim-meter
Fix aim error meter applying incorrect scaling constant in normalised mode
2025-08-26 15:16:15 +09:00
Bartłomiej Dach
e908b80359 Fix aim error meter applying incorrect scaling constant in relative mode
Closes https://github.com/ppy/osu/issues/34769

Visible (and easiest to check) in test scene.
2025-08-25 14:20:05 +02:00
Bartłomiej Dach
a2bf8e3988 Fix copy-paste fail in log message 2025-08-25 13:43:03 +02:00
Bartłomiej Dach
6e8246b539 Merge pull request #34761 from frenzibyte/fix-flashlight
Fix flashlight not always matching gameplay scaling
2025-08-25 12:03:58 +02:00
Salman Alshamrani
3cca458c21 Fix xmldoc error and reword 2025-08-24 18:55:45 +03:00
Salman Alshamrani
bc59270f3e Fix flashlight not handling internal playfield sizing changes
Note that this does not handle sizing/scaling changes applied directly
to `Playfield`, but it handles any changes within the layers inside
`PlayfieldAdjustmentContainer`.
2025-08-24 17:51:05 +03:00
Salman Alshamrani
c0c3690908 Remove no longer valid test 2025-08-23 09:28:14 +03:00
Salman Alshamrani
73624e4e25 Add visual test setup for taiko flashlight 2025-08-21 19:03:43 +03:00
Salman Alshamrani
f374af7ce7 Fix taiko flashlight applying aspect ratio twice 2025-08-21 19:03:43 +03:00
Salman Alshamrani
7530ad1a7b Adjust default flashlight size on osu! & osu!catch
Because the flashlight is made to be scaled by playfield, there are
constant scale factors applied somewhere in the
`PlayfieldAdjustmentContainer` which needs to be reflected in the
flashlight size to keep the size the same.

The factor is specifically 1.6x, computed in {Osu,Catch}PlayfieldAdjustmentContainer.ScalingContainer`.

More generally, I've deduced these factors by logging the difference
between the `flashlightSize` before and after b78abe2f.
2025-08-21 19:03:43 +03:00
Salman Alshamrani
a049f5065d Fix flashlight not correctly scaled to match playfield 2025-08-21 19:03:43 +03:00
13 changed files with 138 additions and 74 deletions

View File

@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
public override float DefaultFlashlightSize => 325;
public override float DefaultFlashlightSize => 203.125f;
protected override Flashlight CreateFlashlight() => new CatchFlashlight(this, playfield);

View File

@@ -32,26 +32,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
[Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
[Test]
public void TestPlayfieldBasedSize()
{
OsuModFlashlight flashlight;
CreateModTest(new ModTestData
{
Mods = [flashlight = new OsuModFlashlight(), new OsuModBarrelRoll()],
PassCondition = () =>
{
var flashlightOverlay = Player.DrawableRuleset.Overlays
.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>()
.First();
// the combo check is here because the flashlight radius decreases for the first time at 100 combo
// and hardcoding it here eliminates the need to meddle in flashlight internals further by e.g. exposing `GetComboScaleFor()`
return flashlightOverlay.GetSize() < flashlight.DefaultFlashlightSize && Player.GameplayState.ScoreProcessor.Combo.Value < 100;
}
});
}
[Test]
public void TestSliderDimsOnlyAfterStartTime()
{

View File

@@ -323,7 +323,7 @@ namespace osu.Game.Rulesets.Osu.HUD
if (PositionDisplayStyle.Value == PositionDisplay.Normalised && lastObjectPosition != null)
{
hitPosition = AccuracyHeatmap.FindRelativeHitPosition(lastObjectPosition.Value, ((OsuHitObject)circleJudgement.HitObject).StackedEndPosition,
circleJudgement.CursorPositionAtHit.Value, objectRadius, 45) * 0.5f;
circleJudgement.CursorPositionAtHit.Value, objectRadius, 45) * (inner_portion / 2);
}
else
{

View File

@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
public override float DefaultFlashlightSize => 200;
public override float DefaultFlashlightSize => 125;
private OsuFlashlight flashlight = null!;

View File

@@ -3,7 +3,10 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.UI;
using osuTK;
@@ -12,6 +15,34 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods
{
public partial class TestSceneTaikoModFlashlight : TaikoModTestScene
{
[Test]
public void TestAspectRatios([Values] bool withClassicMod)
{
if (withClassicMod)
CreateModTest(new ModTestData { Mods = new Mod[] { new TaikoModFlashlight(), new TaikoModClassic() }, PassCondition = () => true });
else
CreateModTest(new ModTestData { Mod = new TaikoModFlashlight(), PassCondition = () => true });
AddStep("clear dim", () => LocalConfig.SetValue(OsuSetting.DimLevel, 0.0));
AddStep("reset", () => Stack.FillMode = FillMode.Stretch);
AddStep("set to 16:9", () =>
{
Stack.FillAspectRatio = 16 / 9f;
Stack.FillMode = FillMode.Fit;
});
AddStep("set to 4:3", () =>
{
Stack.FillAspectRatio = 4 / 3f;
Stack.FillMode = FillMode.Fit;
});
AddSliderStep("aspect ratio", 0.01f, 5f, 1f, v =>
{
Stack.FillAspectRatio = v;
Stack.FillMode = FillMode.Fit;
});
}
[TestCase(1f)]
[TestCase(0.5f)]
[TestCase(1.25f)]

View File

@@ -47,28 +47,15 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
this.taikoPlayfield = taikoPlayfield;
FlashlightSize = adjustSizeForPlayfieldAspectRatio(GetSize());
FlashlightSize = new Vector2(0, GetSize());
FlashlightSmoothness = 1.4f;
AddLayout(flashlightProperties);
}
/// <summary>
/// Returns the aspect ratio-adjusted size of the flashlight.
/// This ensures that the size of the flashlight remains independent of taiko-specific aspect ratio adjustments.
/// </summary>
/// <param name="size">
/// The size of the flashlight.
/// The value provided here should always come from <see cref="ModFlashlight{T}.Flashlight.GetSize"/>.
/// </param>
private Vector2 adjustSizeForPlayfieldAspectRatio(float size)
{
return new Vector2(0, size * taikoPlayfield.Parent!.Scale.Y);
}
protected override void UpdateFlashlightSize(float size)
{
this.TransformTo(nameof(FlashlightSize), adjustSizeForPlayfieldAspectRatio(size), FLASHLIGHT_FADE_DURATION);
this.TransformTo(nameof(FlashlightSize), new Vector2(0, size), FLASHLIGHT_FADE_DURATION);
}
protected override string FragmentShader => "CircularFlashlight";
@@ -82,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
FlashlightPosition = ToLocalSpace(taikoPlayfield.HitTarget.ScreenSpaceDrawQuad.Centre);
ClearTransforms(targetMember: nameof(FlashlightSize));
FlashlightSize = adjustSizeForPlayfieldAspectRatio(GetSize());
FlashlightSize = new Vector2(0, GetSize());
flashlightProperties.Validate();
}

View File

@@ -24,6 +24,14 @@ namespace osu.Game.Tests.Visual.Gameplay
{
protected TestReplayPlayer Player = null!;
[Test]
public void TestFailedBeatmapLoad()
{
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo, withHitObjects: false));
AddUntilStep("wait for exit", () => Player.IsCurrentScreen());
}
[Test]
public void TestPauseViaSpace()
{

View File

@@ -727,7 +727,7 @@ namespace osu.Game.Database
}
catch (Exception e)
{
Logger.Log(@$"Failed to update ranked/submitted dates for beatmap set {id}: {e}");
Logger.Log(@$"Failed to update user tags for beatmap {id}: {e}");
++failedCount;
}
}

View File

@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -14,8 +13,8 @@ using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Layout;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.OpenGL.Vertices;
@@ -85,7 +84,11 @@ namespace osu.Game.Rulesets.Mods
flashlight.Colour = Color4.Black;
flashlight.Combo.BindTo(Combo);
flashlight.GetPlayfieldScale = () => drawableRuleset.PlayfieldAdjustmentContainer.Scale;
var playfieldDrawInfoTracker = new PlayfieldDrawInfoTracker();
drawableRuleset.PlayfieldAdjustmentContainer.Add(playfieldDrawInfoTracker);
flashlight.PlayfieldDrawInfoTracker = playfieldDrawInfoTracker;
drawableRuleset.Overlays.Add(new Container
{
@@ -111,7 +114,9 @@ namespace osu.Game.Rulesets.Mods
public override bool RemoveCompletedTransforms => false;
internal Func<Vector2>? GetPlayfieldScale;
internal PlayfieldDrawInfoTracker PlayfieldDrawInfoTracker { get; set; } = null!;
private DrawInfo playfieldDrawInfo => PlayfieldDrawInfoTracker.DrawInfo;
private readonly float defaultFlashlightSize;
private readonly float sizeMultiplier;
@@ -146,6 +151,8 @@ namespace osu.Game.Rulesets.Mods
isBreakTime.BindTo(player.IsBreakTime);
isBreakTime.BindValueChanged(_ => UpdateFlashlightSize(GetSize()), true);
}
PlayfieldDrawInfoTracker.OnDrawInfoInvalidate += () => Invalidate(Invalidation.DrawNode);
}
protected abstract void UpdateFlashlightSize(float size);
@@ -156,15 +163,6 @@ namespace osu.Game.Rulesets.Mods
{
float size = defaultFlashlightSize * sizeMultiplier;
if (GetPlayfieldScale != null)
{
Vector2 playfieldScale = GetPlayfieldScale();
Debug.Assert(Precision.AlmostEquals(Math.Abs(playfieldScale.X), Math.Abs(playfieldScale.Y)),
@"Playfield has non-proportional scaling. Flashlight implementations should be revisited with regard to balance.");
size *= Math.Abs(playfieldScale.X);
}
if (isBreakTime.Value)
size *= 2.5f;
else if (comboBasedSize)
@@ -265,7 +263,11 @@ namespace osu.Game.Rulesets.Mods
shader = Source.shader;
screenSpaceDrawQuad = Source.ScreenSpaceDrawQuad;
flashlightPosition = Vector2Extensions.Transform(Source.FlashlightPosition, DrawInfo.Matrix);
flashlightSize = Source.FlashlightSize * DrawInfo.Matrix.ExtractScale().Xy;
// scale the flashlight based on the playfield to match gameplay components scale.
Vector2 drawInfoScale = Source.playfieldDrawInfo.Matrix.ExtractScale().Xy;
flashlightSize = Source.FlashlightSize * drawInfoScale;
flashlightDim = Source.FlashlightDim;
flashlightSmoothness = Source.flashlightSmoothness;
}
@@ -321,5 +323,33 @@ namespace osu.Game.Rulesets.Mods
}
}
}
/// <summary>
/// The purpose of this component is to track any changes to <c>Playfield.Parent.DrawInfo</c>
/// (by being added to the content of <see cref="PlayfieldAdjustmentContainer"/>).
/// All in order for the flashlight to invalidate its draw node and read any changes in the playfield's scaling.
/// </summary>
internal partial class PlayfieldDrawInfoTracker : Component
{
private readonly LayoutValue drawInfoLayout = new LayoutValue(Invalidation.DrawInfo);
public Action? OnDrawInfoInvalidate;
public PlayfieldDrawInfoTracker()
{
AddLayout(drawInfoLayout);
}
protected override void Update()
{
base.Update();
if (!drawInfoLayout.IsValid)
{
OnDrawInfoInvalidate?.Invoke();
drawInfoLayout.Validate();
}
}
}
}
}

View File

@@ -314,6 +314,7 @@ namespace osu.Game.Screens.Menu
logoColour = config.GetBindable<Colour4>(OsuSetting.MenuCookieColor);
logoColour.BindValueChanged(_ => UpdateColour());
UpdateColour();
}
private int lastBeatIndex;

View File

@@ -1,8 +1,6 @@
// 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.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
@@ -31,17 +29,19 @@ namespace osu.Game.Screens.Play
private readonly Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore;
private PlaybackSettings playbackSettings;
[Cached(typeof(IGameplayLeaderboardProvider))]
private readonly SoloGameplayLeaderboardProvider leaderboardProvider = new SoloGameplayLeaderboardProvider();
protected override UserActivity InitialActivity => new UserActivity.WatchingReplay(Score.ScoreInfo);
protected override UserActivity? InitialActivity =>
// score may be null if LoadedBeatmapSuccessfully is false.
Score == null ? null : new UserActivity.WatchingReplay(Score.ScoreInfo);
private bool isAutoplayPlayback => GameplayState.Mods.OfType<ModAutoplay>().Any();
private double? lastFrameTime;
private ReplayFailIndicator failIndicator;
private ReplayFailIndicator? failIndicator;
private PlaybackSettings? playbackSettings;
protected override bool CheckModsAllowFailure()
{
@@ -60,12 +60,12 @@ namespace osu.Game.Screens.Play
return false;
}
public ReplayPlayer(Score score, PlayerConfiguration configuration = null)
public ReplayPlayer(Score score, PlayerConfiguration? configuration = null)
: this((_, _) => score, configuration)
{
}
public ReplayPlayer(Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore, PlayerConfiguration configuration = null)
public ReplayPlayer(Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore, PlayerConfiguration? configuration = null)
: base(configuration)
{
this.createScore = createScore;
@@ -133,6 +133,9 @@ namespace osu.Game.Screens.Play
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (!LoadedBeatmapSuccessfully)
return false;
switch (e.Action)
{
case GlobalAction.StepReplayBackward:
@@ -144,11 +147,11 @@ namespace osu.Game.Screens.Play
return true;
case GlobalAction.SeekReplayBackward:
SeekInDirection(-5 * (float)playbackSettings.UserPlaybackRate.Value);
SeekInDirection(-5 * (float)playbackSettings!.UserPlaybackRate.Value);
return true;
case GlobalAction.SeekReplayForward:
SeekInDirection(5 * (float)playbackSettings.UserPlaybackRate.Value);
SeekInDirection(5 * (float)playbackSettings!.UserPlaybackRate.Value);
return true;
case GlobalAction.TogglePauseReplay:
@@ -192,7 +195,7 @@ namespace osu.Game.Screens.Play
{
// base logic intentionally suppressed - we have our own custom fail interaction
ScoreProcessor.FailScore(Score.ScoreInfo);
failIndicator.Display();
failIndicator!.Display();
}
public override void OnSuspending(ScreenTransitionEvent e)
@@ -204,18 +207,18 @@ namespace osu.Game.Screens.Play
public override bool OnExiting(ScreenExitEvent e)
{
// safety against filters or samples from the indicator playing long after the screen is exited
failIndicator.RemoveAndDisposeImmediately();
failIndicator?.RemoveAndDisposeImmediately();
return base.OnExiting(e);
}
private void stopAllAudioEffects()
{
// safety against filters or samples from the indicator playing long after the screen is exited
failIndicator.RemoveAndDisposeImmediately();
failIndicator?.RemoveAndDisposeImmediately();
if (GameplayClockContainer is MasterGameplayClockContainer master)
{
playbackSettings.UserPlaybackRate.UnbindFrom(master.UserPlaybackRate);
playbackSettings?.UserPlaybackRate.UnbindFrom(master.UserPlaybackRate);
master.UserPlaybackRate.SetDefault();
}
}

View File

@@ -37,7 +37,11 @@ namespace osu.Game.Screens.Select.Leaderboards
var scoresToShow = new List<GameplayLeaderboardScore>();
var scoresRequest = new IndexPlaylistScoresRequest(room.RoomID!.Value, playlistItem.ID);
scoresRequest.Success += response =>
api.Perform(scoresRequest);
var response = scoresRequest.Response;
if (response != null)
{
isPartial = response.Scores.Count < response.TotalScores;
@@ -50,8 +54,7 @@ namespace osu.Game.Screens.Select.Leaderboards
if (response.UserScore != null && response.Scores.All(s => s.ID != response.UserScore.ID))
scoresToShow.Add(new GameplayLeaderboardScore(response.UserScore, tracked: false, GameplayLeaderboardScore.ComboDisplayMode.Highest));
};
api.Perform(scoresRequest);
}
if (gameplayState != null)
{
@@ -62,8 +65,12 @@ namespace osu.Game.Screens.Select.Leaderboards
// touching the public bindable must happen on the update thread for general thread safety,
// since we may have external subscribers bound already
Schedule(() => scores.AddRange(scoresToShow));
Scheduler.AddDelayed(sort, 1000, true);
Schedule(() =>
{
scores.AddRange(scoresToShow);
sort();
Scheduler.AddDelayed(sort, 1000, true);
});
}
// logic shared with SoloGameplayLeaderboardProvider

View File

@@ -37,7 +37,21 @@ namespace osu.Game.Screens.SelectV2
get => working;
set
{
if (value == working)
if (working == null && value == null)
return;
// this guard papers over excessive refreshes of the background asset which occur if `working == value` type guards are used.
// the root cause of why `working == value` type guards fail here is that `SongSelect` will invalidate working beatmaps very often
// (via https://github.com/ppy/osu/blob/d3ae20dd882381e109c20ca00ee5237e4dd1750d/osu.Game/Screens/SelectV2/SongSelect.cs#L506-L507),
// due to a variety of causes, ranging from "someone typed a letter in the search box" (which triggers a refilter -> presentation of new items -> `ensureGlobalBeatmapValid()`),
// to "someone just went into the editor and replaced every single file in the set, including the background".
// the following guard approximates the most appropriate debounce criterion, which is the contents of the actual asset that is supposed to be displayed in the background,
// i.e. if the hash of the new background file matches the old, then we do not bother updating the working beatmap here.
//
// note that this is basically a reimplementation of the caching scheme in `WorkingBeatmapCache.getBackgroundFromStore()`,
// which cannot be used directly by retrieving the texture and checking texture reference equality,
// because missing the cache would incur a synchronous texture load on the update thread.
if (getBackgroundFileHash(working) == getBackgroundFileHash(value))
return;
working = value;
@@ -52,6 +66,9 @@ namespace osu.Game.Screens.SelectV2
}
}
private static string? getBackgroundFileHash(WorkingBeatmap? working)
=> working?.BeatmapSetInfo.GetFile(working.Metadata.BackgroundFile)?.File.Hash;
public PanelSetBackground()
{
RelativeSizeAxes = Axes.Both;