28 Commits

Author SHA1 Message Date
02e7000ee4 update editor beatmap anonymization code to update not just first diff 2025-08-30 22:56:01 +03:00
081355864e add beatmap editor option to remove online data of a map 2025-08-29 20:19:39 +03:00
6435a835d1 fix background category selection (this time for real) 2025-08-29 18:48:13 +03:00
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
5399943118 update logo colour only when changing setting value 2025-08-27 00:58:26 +03:00
d07f82f6f4 refactor custom seasonal background code
some of it may be trauma-inducing, but I don't know how to make it
better
2025-08-27 00:43:26 +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
5b186bb740 potentially make logo look less weird (untested)
uh, yeah, I accidentally flipped the colors around in the UpdateColour() method (which I should've probably make private or protected), and it's the reason why the logo overall looked dimmer than it should've

anyhow, this should *probably* look a bit better, don't have any means to test it yet though
2025-08-25 12:46:27 +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
19 changed files with 253 additions and 160 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"dotnet.defaultSolution": "osu.Desktop.slnf"
}

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

@@ -198,6 +198,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin);
SetDefault(OsuSetting.SeasonalBackgroundMode, SeasonalBackgroundMode.Never);
SetDefault(OsuSetting.UseSeasonalBackgroundsV2, true);
SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full);
@@ -442,6 +443,7 @@ namespace osu.Game.Configuration
MenuBackgroundSource,
GameplayDisableWinKey,
SeasonalBackgroundMode,
UseSeasonalBackgroundsV2, // TODO: add migrations
BackgroundCategory,
EditorWaveformOpacity,
EditorShowHitMarkers,

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

@@ -33,19 +33,19 @@ namespace osu.Game.Graphics.Backgrounds
[Resolved]
private IAPIProvider api { get; set; }
private Bindable<SeasonalBackgroundMode> backgroundMode;
private Bindable<bool> useSeasonalBackgrounds;
private Bindable<string> selectedCategory;
private Bindable<APISeasonalBackgrounds> currentBackgrounds;
private int currentBackgroundIndex;
private bool shouldShowCustomBackgrounds => backgroundMode.Value != SeasonalBackgroundMode.Never;
private bool shouldShowCustomBackgrounds => useSeasonalBackgrounds.Value;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, SessionStatics sessionStatics)
{
backgroundMode = config.GetBindable<SeasonalBackgroundMode>(OsuSetting.SeasonalBackgroundMode);
backgroundMode.BindValueChanged(_ => BackgroundChanged?.Invoke());
useSeasonalBackgrounds = config.GetBindable<bool>(OsuSetting.UseSeasonalBackgroundsV2);
useSeasonalBackgrounds.BindValueChanged(_ => BackgroundChanged?.Invoke());
selectedCategory = config.GetBindable<string>(OsuSetting.BackgroundCategory);
selectedCategory.BindValueChanged(_ => fetchBackgroundsForSelectedCategory());
@@ -53,18 +53,18 @@ namespace osu.Game.Graphics.Backgrounds
currentBackgrounds = sessionStatics.GetBindable<APISeasonalBackgrounds>(Static.SeasonalBackgrounds);
if (shouldShowCustomBackgrounds)
fetchCategories();
fetchCategories(true);
}
/// <summary>
/// Public method to trigger a refresh of categories from the UI.
/// </summary>
public void RefreshCategories()
public void RefreshCategories(bool ignoreSuccess = false)
{
fetchCategories();
fetchCategories(ignoreSuccess);
}
private void fetchCategories()
private void fetchCategories(bool ignoreSuccess = false)
{
if (!shouldShowCustomBackgrounds) return;
@@ -74,20 +74,28 @@ namespace osu.Game.Graphics.Backgrounds
{
var serverCategories = response.Categories ?? Enumerable.Empty<string>();
AvailableCategories.Value = new[] { "Default" }.Concat(serverCategories)
.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
AvailableCategories.Value = serverCategories.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
if (!AvailableCategories.Value.Any())
{
selectedCategory.Value = "";
return; // we don't have any categories!!!
}
if (!AvailableCategories.Value.Contains(selectedCategory.Value))
selectedCategory.Value = "Default";
else
selectedCategory.Value = AvailableCategories.Value.Contains("Default")
? "Default"
: AvailableCategories.Value.ElementAt(0);
fetchBackgroundsForSelectedCategory();
if (!ignoreSuccess)
OnCategoriesRefreshed?.Invoke();
};
request.Failure += exception =>
{
AvailableCategories.Value = new[] { "Íå óäàëîñü çàãðóçèòü..." };
AvailableCategories.Value = Array.Empty<string>();
OnLoadFailure?.Invoke(exception);
};
@@ -98,13 +106,6 @@ namespace osu.Game.Graphics.Backgrounds
{
if (!shouldShowCustomBackgrounds) return;
if (AvailableCategories.Value.Count() == 1 && AvailableCategories.Value.First().Contains("Íå óäàëîñü"))
{
currentBackgrounds.Value = new APISeasonalBackgrounds { Backgrounds = new List<APISeasonalBackground>() };
BackgroundChanged?.Invoke();
return;
}
string categoryToFetch = selectedCategory.Value == "Default" ? null : selectedCategory.Value;
var request = new GetSeasonalBackgroundsRequest(categoryToFetch);

View File

@@ -171,7 +171,7 @@ namespace osu.Game
private readonly ScreenshotManager screenshotManager = new ScreenshotManager();
[Cached]
private readonly SeasonalBackgroundLoader backgroundLoader;
private SeasonalBackgroundLoader seasonalBackgroundLoader;
protected SentryLogger SentryLogger;
@@ -253,10 +253,6 @@ namespace osu.Game
public OsuGame(string[] args = null)
{
backgroundLoader = new SeasonalBackgroundLoader();
backgroundLoader.OnLoadFailure += handleBackgroundLoadFailure;
backgroundLoader.OnCategoriesRefreshed += handleCategoriesRefreshed;
this.args = args;
Logger.NewEntry += forwardGeneralLogToNotifications;
@@ -271,6 +267,8 @@ namespace osu.Game
tabletLogNotifyOnError = true;
}, true);
});
initializeSeasonalBackgrounds();
}
#region IOverlayManager
@@ -409,18 +407,6 @@ namespace osu.Game
}
}
}
private void handleCategoriesRefreshed()
{
Schedule(() =>
{
Notifications?.Post(new SimpleNotification
{
Text = ButtonSystemStrings.SeasonalBackgroundsRefreshed,
Icon = FontAwesome.Solid.CheckCircle,
Transient = true
});
});
}
[BackgroundDependencyLoader]
private void load()
@@ -1252,7 +1238,7 @@ namespace osu.Game
loadComponentSingleFile(screenshotManager, Add);
loadComponentSingleFile(backgroundLoader, Add);
loadComponentSingleFile(seasonalBackgroundLoader, Add);
// dependency on notification overlay, dependent by settings overlay
loadComponentSingleFile(CreateUpdateManager(), Add, true);
@@ -1355,18 +1341,6 @@ namespace osu.Game
handleStartupImport();
}
private void handleBackgroundLoadFailure(Exception exception)
{
Schedule(() =>
{
Notifications?.Post(new SimpleErrorNotification
{
Text = ButtonSystemStrings.SeasonalBackgroundsFail,
Icon = FontAwesome.Solid.ExclamationTriangle,
Transient = true
});
});
}
private void handleBackButton()
{
// TODO: this is SUPER SUPER bad.
@@ -1503,6 +1477,40 @@ namespace osu.Game
}
}
private void initializeSeasonalBackgrounds()
{
seasonalBackgroundLoader = new SeasonalBackgroundLoader();
seasonalBackgroundLoader.OnCategoriesRefreshed += handleCategoriesRefreshed;
seasonalBackgroundLoader.OnLoadFailure += handleBackgroundLoadFailure;
}
private void handleCategoriesRefreshed()
{
Schedule(() =>
{
Notifications?.Post(new SimpleNotification
{
Text = ButtonSystemStrings.SeasonalBackgroundsRefreshed,
Icon = FontAwesome.Solid.CheckCircle,
Transient = true
});
});
}
private void handleBackgroundLoadFailure(Exception exception)
{
Schedule(() =>
{
Notifications?.Post(new SimpleErrorNotification
{
Text = ButtonSystemStrings.SeasonalBackgroundsFail,
Icon = FontAwesome.Solid.ExclamationTriangle,
Transient = true
});
});
}
private Task asyncLoadStream;
/// <summary>

View File

@@ -5,16 +5,13 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Settings;
namespace osu.Game.Overlays.Settings.Sections.UserInterface
{
@@ -29,21 +26,19 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
private SettingsEnumDropdown<BackgroundSource> backgroundSourceDropdown;
private Bindable<bool> useSeasonalBackgrounds;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, IAPIProvider api)
{
user = api.LocalUser.GetBoundCopy();
var backgroundModeBindable = config.GetBindable<SeasonalBackgroundMode>(OsuSetting.SeasonalBackgroundMode);
var enabledProxyBindable = new Bindable<bool>();
backgroundModeBindable.BindValueChanged(mode => enabledProxyBindable.Value = mode.NewValue == SeasonalBackgroundMode.Always, true);
enabledProxyBindable.BindValueChanged(enabled => backgroundModeBindable.Value = enabled.NewValue ? SeasonalBackgroundMode.Always : SeasonalBackgroundMode.Never);
useSeasonalBackgrounds = config.GetBindable<bool>(OsuSetting.UseSeasonalBackgroundsV2);
var backgroundToggle = new SettingsCheckbox
{
LabelText = UserInterfaceStrings.UseSeasonalBackgrounds,
Current = enabledProxyBindable
Current = config.GetBindable<bool>(OsuSetting.UseSeasonalBackgroundsV2),
ClassicDefault = true
};
var categoryDropdown = new SettingsDropdown<string>
@@ -58,28 +53,21 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
Action = () => backgroundLoader.RefreshCategories()
};
backgroundLoader.AvailableCategories.BindValueChanged(categories => categoryDropdown.Items = categories.NewValue, true);
// TODO: the category dropdown disappear if no backgrounds (e.g. when first enabling the setting)
refreshButton.CanBeShown.BindTo(useSeasonalBackgrounds);
categoryDropdown.CanBeShown.BindTo(useSeasonalBackgrounds);
useSeasonalBackgrounds.BindValueChanged(
_ => backgroundLoader.RefreshCategories(true)
);
backgroundModeBindable.BindValueChanged(mode =>
{
if (mode.NewValue == SeasonalBackgroundMode.Always)
{
categoryDropdown.Show();
refreshButton.Show();
}
else
{
categoryDropdown.Hide();
refreshButton.Hide();
}
}, true);
backgroundLoader.AvailableCategories.BindValueChanged(categories => categoryDropdown.Items = categories.NewValue, true);
Children = new Drawable[]
{
new SettingsCheckbox
{
LabelText = UserInterfaceStrings.ShowMenuTips,
Current = config.GetBindable<bool>(OsuSetting.MenuTips)
Current = config.GetBindable<bool>(OsuSetting.MenuTips),
},
new SettingsCheckbox
{

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

@@ -40,6 +40,7 @@ using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.OSD;
using osu.Game.Rulesets;
@@ -1312,6 +1313,8 @@ namespace osu.Game.Screens.Edit
yield return upload;
}
yield return new EditorMenuItem("Remove all online IDs", MenuItemType.Destructive, anonymizeBeatmap);
if (editorBeatmap.BeatmapInfo.OnlineID > 0)
{
yield return new OsuMenuItemSpacer();
@@ -1396,6 +1399,29 @@ namespace osu.Game.Screens.Edit
void startSubmission() => this.Push(new BeatmapSubmissionScreen());
}
private void anonymizeBeatmap()
{
dialogOverlay.Push(new ConfirmDialog(
"Really remove online IDs?", () =>
{
var maps = editorBeatmap.BeatmapInfo.BeatmapSet.Beatmaps;
foreach (BeatmapInfo map in maps)
{
map.OnlineID = -1;
map.BeatmapSet.OnlineID = -1;
map.ResetOnlineInfo(true);
beatmapManager.Save(
map,
beatmapManager.GetWorkingBeatmap(map, true)!.Beatmap,
editorBeatmap.BeatmapSkin
); // as much as I don't want to mutate this much, there's no other choice
}
updateLastSavedHash();
onScreenDisplay?.Display(new BeatmapEditorToast("Online IDs removed", editorBeatmap.BeatmapInfo.GetDisplayTitle()));
}
));
}
private void exportBeatmap(bool legacy)
{
if (HasUnsavedChanges)

View File

@@ -196,7 +196,7 @@ namespace osu.Game.Screens.Menu
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(
Color4Extensions.FromHex(@"ff66ba"), // original osu! cookie pink
Color4Extensions.Darken(Color4Extensions.FromHex(@"ff66ba"), 1.0f)
Color4Extensions.Darken(Color4Extensions.FromHex(@"ff66ba"), 0.5f)
),
},
triangles = new TrianglesV2
@@ -208,7 +208,7 @@ namespace osu.Game.Screens.Menu
SpawnRatio = 1.4f,
Colour = ColourInfo.GradientVertical(
Color4Extensions.FromHex(@"ff66ba"),
Color4Extensions.Darken(Color4Extensions.FromHex(@"ff66ba"), 2.5f)
Color4Extensions.Darken(Color4Extensions.FromHex(@"ff66ba"), 1.0f)
),
RelativeSizeAxes = Axes.Both,
},
@@ -260,19 +260,20 @@ namespace osu.Game.Screens.Menu
};
}
public void UpdateColour() {
public void UpdateColour()
{
if (triangles == null || colourBox == null)
return; // we're still loading
colourBox.Colour = ColourInfo.GradientVertical(
logoColour.Value,
Color4Extensions.Darken(logoColour.Value, 0.5f)
);
triangles.Colour = ColourInfo.GradientVertical(
logoColour.Value,
Color4Extensions.Darken(logoColour.Value, 1.0f)
);
colourBox.Colour = ColourInfo.GradientVertical(
logoColour.Value,
Color4Extensions.Darken(logoColour.Value, 2.5f)
);
}
public Container LogoElements { get; private set; }
@@ -313,6 +314,8 @@ namespace osu.Game.Screens.Menu
ripple.Texture = textures.Get(@"Menu/logo");
logoColour = config.GetBindable<Colour4>(OsuSetting.MenuCookieColor);
logoColour.BindValueChanged(_ => UpdateColour());
UpdateColour();
}
private int lastBeatIndex;
@@ -393,7 +396,6 @@ namespace osu.Game.Screens.Menu
protected override void Update()
{
base.Update();
UpdateColour();
const float scale_adjust_cutoff = 0.4f;

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));
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;