27 Commits

Author SHA1 Message Date
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 238 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 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); protected override Flashlight CreateFlashlight() => new CatchFlashlight(this, playfield);

View File

@@ -32,26 +32,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
[Test] [Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true }); 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] [Test]
public void TestSliderDimsOnlyAfterStartTime() public void TestSliderDimsOnlyAfterStartTime()
{ {

View File

@@ -323,7 +323,7 @@ namespace osu.Game.Rulesets.Osu.HUD
if (PositionDisplayStyle.Value == PositionDisplay.Normalised && lastObjectPosition != null) if (PositionDisplayStyle.Value == PositionDisplay.Normalised && lastObjectPosition != null)
{ {
hitPosition = AccuracyHeatmap.FindRelativeHitPosition(lastObjectPosition.Value, ((OsuHitObject)circleJudgement.HitObject).StackedEndPosition, 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 else
{ {

View File

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

View File

@@ -3,7 +3,10 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.Taiko.UI;
using osuTK; using osuTK;
@@ -12,6 +15,34 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods
{ {
public partial class TestSceneTaikoModFlashlight : TaikoModTestScene 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(1f)]
[TestCase(0.5f)] [TestCase(0.5f)]
[TestCase(1.25f)] [TestCase(1.25f)]

View File

@@ -47,28 +47,15 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
this.taikoPlayfield = taikoPlayfield; this.taikoPlayfield = taikoPlayfield;
FlashlightSize = adjustSizeForPlayfieldAspectRatio(GetSize()); FlashlightSize = new Vector2(0, GetSize());
FlashlightSmoothness = 1.4f; FlashlightSmoothness = 1.4f;
AddLayout(flashlightProperties); 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) 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"; protected override string FragmentShader => "CircularFlashlight";
@@ -82,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
FlashlightPosition = ToLocalSpace(taikoPlayfield.HitTarget.ScreenSpaceDrawQuad.Centre); FlashlightPosition = ToLocalSpace(taikoPlayfield.HitTarget.ScreenSpaceDrawQuad.Centre);
ClearTransforms(targetMember: nameof(FlashlightSize)); ClearTransforms(targetMember: nameof(FlashlightSize));
FlashlightSize = adjustSizeForPlayfieldAspectRatio(GetSize()); FlashlightSize = new Vector2(0, GetSize());
flashlightProperties.Validate(); flashlightProperties.Validate();
} }

View File

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

View File

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

View File

@@ -727,7 +727,7 @@ namespace osu.Game.Database
} }
catch (Exception e) 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; ++failedCount;
} }
} }

View File

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

View File

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

View File

@@ -5,16 +5,13 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Settings;
namespace osu.Game.Overlays.Settings.Sections.UserInterface namespace osu.Game.Overlays.Settings.Sections.UserInterface
{ {
@@ -29,21 +26,19 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
private SettingsEnumDropdown<BackgroundSource> backgroundSourceDropdown; private SettingsEnumDropdown<BackgroundSource> backgroundSourceDropdown;
private Bindable<bool> useSeasonalBackgrounds;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config, IAPIProvider api) private void load(OsuConfigManager config, IAPIProvider api)
{ {
user = api.LocalUser.GetBoundCopy(); user = api.LocalUser.GetBoundCopy();
useSeasonalBackgrounds = config.GetBindable<bool>(OsuSetting.UseSeasonalBackgroundsV2);
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);
var backgroundToggle = new SettingsCheckbox var backgroundToggle = new SettingsCheckbox
{ {
LabelText = UserInterfaceStrings.UseSeasonalBackgrounds, LabelText = UserInterfaceStrings.UseSeasonalBackgrounds,
Current = enabledProxyBindable Current = config.GetBindable<bool>(OsuSetting.UseSeasonalBackgroundsV2),
ClassicDefault = true
}; };
var categoryDropdown = new SettingsDropdown<string> var categoryDropdown = new SettingsDropdown<string>
@@ -58,28 +53,21 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
Action = () => backgroundLoader.RefreshCategories() 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 => backgroundLoader.AvailableCategories.BindValueChanged(categories => categoryDropdown.Items = categories.NewValue, true);
{
if (mode.NewValue == SeasonalBackgroundMode.Always)
{
categoryDropdown.Show();
refreshButton.Show();
}
else
{
categoryDropdown.Hide();
refreshButton.Hide();
}
}, true);
Children = new Drawable[] Children = new Drawable[]
{ {
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = UserInterfaceStrings.ShowMenuTips, LabelText = UserInterfaceStrings.ShowMenuTips,
Current = config.GetBindable<bool>(OsuSetting.MenuTips) Current = config.GetBindable<bool>(OsuSetting.MenuTips),
}, },
new SettingsCheckbox new SettingsCheckbox
{ {

View File

@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@@ -14,8 +13,8 @@ using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Shaders.Types; using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Layout;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.OpenGL.Vertices; using osu.Game.Graphics.OpenGL.Vertices;
@@ -85,7 +84,11 @@ namespace osu.Game.Rulesets.Mods
flashlight.Colour = Color4.Black; flashlight.Colour = Color4.Black;
flashlight.Combo.BindTo(Combo); 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 drawableRuleset.Overlays.Add(new Container
{ {
@@ -111,7 +114,9 @@ namespace osu.Game.Rulesets.Mods
public override bool RemoveCompletedTransforms => false; 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 defaultFlashlightSize;
private readonly float sizeMultiplier; private readonly float sizeMultiplier;
@@ -146,6 +151,8 @@ namespace osu.Game.Rulesets.Mods
isBreakTime.BindTo(player.IsBreakTime); isBreakTime.BindTo(player.IsBreakTime);
isBreakTime.BindValueChanged(_ => UpdateFlashlightSize(GetSize()), true); isBreakTime.BindValueChanged(_ => UpdateFlashlightSize(GetSize()), true);
} }
PlayfieldDrawInfoTracker.OnDrawInfoInvalidate += () => Invalidate(Invalidation.DrawNode);
} }
protected abstract void UpdateFlashlightSize(float size); protected abstract void UpdateFlashlightSize(float size);
@@ -156,15 +163,6 @@ namespace osu.Game.Rulesets.Mods
{ {
float size = defaultFlashlightSize * sizeMultiplier; 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) if (isBreakTime.Value)
size *= 2.5f; size *= 2.5f;
else if (comboBasedSize) else if (comboBasedSize)
@@ -265,7 +263,11 @@ namespace osu.Game.Rulesets.Mods
shader = Source.shader; shader = Source.shader;
screenSpaceDrawQuad = Source.ScreenSpaceDrawQuad; screenSpaceDrawQuad = Source.ScreenSpaceDrawQuad;
flashlightPosition = Vector2Extensions.Transform(Source.FlashlightPosition, DrawInfo.Matrix); 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; flashlightDim = Source.FlashlightDim;
flashlightSmoothness = Source.flashlightSmoothness; 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.API;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.OSD; using osu.Game.Overlays.OSD;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@@ -1312,6 +1313,8 @@ namespace osu.Game.Screens.Edit
yield return upload; yield return upload;
} }
yield return new EditorMenuItem("Remove all online IDs", MenuItemType.Destructive, anonymizeBeatmap);
if (editorBeatmap.BeatmapInfo.OnlineID > 0) if (editorBeatmap.BeatmapInfo.OnlineID > 0)
{ {
yield return new OsuMenuItemSpacer(); yield return new OsuMenuItemSpacer();
@@ -1396,6 +1399,14 @@ namespace osu.Game.Screens.Edit
void startSubmission() => this.Push(new BeatmapSubmissionScreen()); void startSubmission() => this.Push(new BeatmapSubmissionScreen());
} }
private void anonymizeBeatmap()
{
dialogOverlay.Push(new ConfirmDialog(
"Really remove online IDs?",
() => playableBeatmap.BeatmapInfo.ResetOnlineInfo(true)
));
}
private void exportBeatmap(bool legacy) private void exportBeatmap(bool legacy)
{ {
if (HasUnsavedChanges) if (HasUnsavedChanges)

View File

@@ -196,7 +196,7 @@ namespace osu.Game.Screens.Menu
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical( Colour = ColourInfo.GradientVertical(
Color4Extensions.FromHex(@"ff66ba"), // original osu! cookie pink Color4Extensions.FromHex(@"ff66ba"), // original osu! cookie pink
Color4Extensions.Darken(Color4Extensions.FromHex(@"ff66ba"), 1.0f) Color4Extensions.Darken(Color4Extensions.FromHex(@"ff66ba"), 0.5f)
), ),
}, },
triangles = new TrianglesV2 triangles = new TrianglesV2
@@ -208,7 +208,7 @@ namespace osu.Game.Screens.Menu
SpawnRatio = 1.4f, SpawnRatio = 1.4f,
Colour = ColourInfo.GradientVertical( Colour = ColourInfo.GradientVertical(
Color4Extensions.FromHex(@"ff66ba"), Color4Extensions.FromHex(@"ff66ba"),
Color4Extensions.Darken(Color4Extensions.FromHex(@"ff66ba"), 2.5f) Color4Extensions.Darken(Color4Extensions.FromHex(@"ff66ba"), 1.0f)
), ),
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
@@ -260,19 +260,20 @@ namespace osu.Game.Screens.Menu
}; };
} }
public void UpdateColour() { public void UpdateColour()
{
if (triangles == null || colourBox == null) if (triangles == null || colourBox == null)
return; // we're still loading return; // we're still loading
colourBox.Colour = ColourInfo.GradientVertical(
logoColour.Value,
Color4Extensions.Darken(logoColour.Value, 0.5f)
);
triangles.Colour = ColourInfo.GradientVertical( triangles.Colour = ColourInfo.GradientVertical(
logoColour.Value, logoColour.Value,
Color4Extensions.Darken(logoColour.Value, 1.0f) Color4Extensions.Darken(logoColour.Value, 1.0f)
); );
colourBox.Colour = ColourInfo.GradientVertical(
logoColour.Value,
Color4Extensions.Darken(logoColour.Value, 2.5f)
);
} }
public Container LogoElements { get; private set; } public Container LogoElements { get; private set; }
@@ -313,6 +314,8 @@ namespace osu.Game.Screens.Menu
ripple.Texture = textures.Get(@"Menu/logo"); ripple.Texture = textures.Get(@"Menu/logo");
logoColour = config.GetBindable<Colour4>(OsuSetting.MenuCookieColor); logoColour = config.GetBindable<Colour4>(OsuSetting.MenuCookieColor);
logoColour.BindValueChanged(_ => UpdateColour());
UpdateColour();
} }
private int lastBeatIndex; private int lastBeatIndex;
@@ -393,7 +396,6 @@ namespace osu.Game.Screens.Menu
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
UpdateColour();
const float scale_adjust_cutoff = 0.4f; 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. // 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.Linq; using System.Linq;
@@ -31,17 +29,19 @@ namespace osu.Game.Screens.Play
private readonly Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore; private readonly Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore;
private PlaybackSettings playbackSettings;
[Cached(typeof(IGameplayLeaderboardProvider))] [Cached(typeof(IGameplayLeaderboardProvider))]
private readonly SoloGameplayLeaderboardProvider leaderboardProvider = new SoloGameplayLeaderboardProvider(); 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 bool isAutoplayPlayback => GameplayState.Mods.OfType<ModAutoplay>().Any();
private double? lastFrameTime; private double? lastFrameTime;
private ReplayFailIndicator failIndicator;
private ReplayFailIndicator? failIndicator;
private PlaybackSettings? playbackSettings;
protected override bool CheckModsAllowFailure() protected override bool CheckModsAllowFailure()
{ {
@@ -60,12 +60,12 @@ namespace osu.Game.Screens.Play
return false; return false;
} }
public ReplayPlayer(Score score, PlayerConfiguration configuration = null) public ReplayPlayer(Score score, PlayerConfiguration? configuration = null)
: this((_, _) => score, configuration) : 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) : base(configuration)
{ {
this.createScore = createScore; this.createScore = createScore;
@@ -133,6 +133,9 @@ namespace osu.Game.Screens.Play
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {
if (!LoadedBeatmapSuccessfully)
return false;
switch (e.Action) switch (e.Action)
{ {
case GlobalAction.StepReplayBackward: case GlobalAction.StepReplayBackward:
@@ -144,11 +147,11 @@ namespace osu.Game.Screens.Play
return true; return true;
case GlobalAction.SeekReplayBackward: case GlobalAction.SeekReplayBackward:
SeekInDirection(-5 * (float)playbackSettings.UserPlaybackRate.Value); SeekInDirection(-5 * (float)playbackSettings!.UserPlaybackRate.Value);
return true; return true;
case GlobalAction.SeekReplayForward: case GlobalAction.SeekReplayForward:
SeekInDirection(5 * (float)playbackSettings.UserPlaybackRate.Value); SeekInDirection(5 * (float)playbackSettings!.UserPlaybackRate.Value);
return true; return true;
case GlobalAction.TogglePauseReplay: case GlobalAction.TogglePauseReplay:
@@ -192,7 +195,7 @@ namespace osu.Game.Screens.Play
{ {
// base logic intentionally suppressed - we have our own custom fail interaction // base logic intentionally suppressed - we have our own custom fail interaction
ScoreProcessor.FailScore(Score.ScoreInfo); ScoreProcessor.FailScore(Score.ScoreInfo);
failIndicator.Display(); failIndicator!.Display();
} }
public override void OnSuspending(ScreenTransitionEvent e) public override void OnSuspending(ScreenTransitionEvent e)
@@ -204,18 +207,18 @@ namespace osu.Game.Screens.Play
public override bool OnExiting(ScreenExitEvent e) public override bool OnExiting(ScreenExitEvent e)
{ {
// safety against filters or samples from the indicator playing long after the screen is exited // safety against filters or samples from the indicator playing long after the screen is exited
failIndicator.RemoveAndDisposeImmediately(); failIndicator?.RemoveAndDisposeImmediately();
return base.OnExiting(e); return base.OnExiting(e);
} }
private void stopAllAudioEffects() private void stopAllAudioEffects()
{ {
// safety against filters or samples from the indicator playing long after the screen is exited // safety against filters or samples from the indicator playing long after the screen is exited
failIndicator.RemoveAndDisposeImmediately(); failIndicator?.RemoveAndDisposeImmediately();
if (GameplayClockContainer is MasterGameplayClockContainer master) if (GameplayClockContainer is MasterGameplayClockContainer master)
{ {
playbackSettings.UserPlaybackRate.UnbindFrom(master.UserPlaybackRate); playbackSettings?.UserPlaybackRate.UnbindFrom(master.UserPlaybackRate);
master.UserPlaybackRate.SetDefault(); master.UserPlaybackRate.SetDefault();
} }
} }

View File

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

View File

@@ -37,7 +37,21 @@ namespace osu.Game.Screens.SelectV2
get => working; get => working;
set 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; return;
working = value; 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() public PanelSetBackground()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;