From 941a4d8994c5a468bc2a4c525dbf98606a9772cb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2026 15:00:50 +0900 Subject: [PATCH 1/8] Make experimental audio the new default (#37856) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Internal offset adjust is based on [survey results](https://docs.google.com/forms/d/1bWdwN9LPB4dJsqh2NO4z0HBiMiod1k8-tJN5oca-1Iw/edit) results (mean: 17.95, median: 24.5) with slight skewing based on cherry-picking results and personal experiences. For users which have had the setting disabled: osu! 2026-05-21 at 09 19 06 For users which are already using it: osu! 2026-05-21 at 09 20 24 Note the button is intentionally hidden to avoid any confusion (it's inverse now, so some users may mistakenly click it). Assume if a user is already on the new engine, they are happy with it. Test migration dialog in startup game flow using: ```diff diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 4bd5ab83a3..091af6d428 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1315,6 +1315,8 @@ protected override void LoadComplete() /// private void applyConfigMigrations() { + dialogOverlay.Push(new MigrateNewAudioDialog(true)); + // arrives as 2020.123.0-lazer string rawVersion = LocalConfig.Get(OsuSetting.Version); ``` --------- Co-authored-by: Bartłomiej Dach --- .../TestSceneMigrateAudioDialog.cs | 39 +++++++++++ osu.Game/Beatmaps/FramedBeatmapClock.cs | 46 ++++++++++++- .../Configuration/MigrateNewAudioDialog.cs | 69 +++++++++++++++++++ osu.Game/Localisation/AudioSettingsStrings.cs | 13 ++-- osu.Game/OsuGame.cs | 20 +++++- osu.Game/Overlays/Dialog/PopupDialog.cs | 9 +++ .../Sections/Audio/AudioDevicesSettings.cs | 62 ++++++++++------- 7 files changed, 221 insertions(+), 37 deletions(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneMigrateAudioDialog.cs create mode 100644 osu.Game/Configuration/MigrateNewAudioDialog.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMigrateAudioDialog.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMigrateAudioDialog.cs new file mode 100644 index 000000000000..ef0f24385a83 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMigrateAudioDialog.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Configuration; +using osu.Game.Overlays; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneMigrateAudioDialog : OsuManualInputManagerTestScene + { + private DialogOverlay overlay = null!; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay()); + } + + [Test] + public void TestWasUsing() + { + AddStep("create dialog", () => + { + overlay.Push(new MigrateNewAudioDialog(true)); + }); + } + + [Test] + public void TestNotUsing() + { + AddStep("create dialog", () => + { + overlay.Push(new MigrateNewAudioDialog(false)); + }); + } + } +} diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 8ce0db6e7bfd..beb924353a37 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using osu.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Timing; @@ -49,6 +50,11 @@ public partial class FramedBeatmapClock : Component, IFrameBasedClock, IAdjustab [Resolved] private IBindable beatmap { get; set; } = null!; + [Resolved] + private AudioManager audioManager { get; set; } = null!; + + private Bindable experimentalAudio = null!; + public bool IsRewinding { get; private set; } public FramedBeatmapClock(bool applyOffsets, bool requireDecoupling, IClock? source = null) @@ -66,9 +72,7 @@ public FramedBeatmapClock(bool applyOffsets, bool requireDecoupling, IClock? sou if (applyOffsets) { - // Audio timings in general with newer BASS versions don't match stable. - // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - platformOffsetClock = new OffsetCorrectionClock(interpolatedTrack) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; + platformOffsetClock = new OffsetCorrectionClock(interpolatedTrack); // User global offset (set in settings) should also be applied. userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock); @@ -94,6 +98,9 @@ protected override void LoadComplete() userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true); + experimentalAudio = audioManager.UseExperimentalWasapi.GetBoundCopy(); + experimentalAudio.BindValueChanged(_ => updatePlatformOffset()); + // TODO: this doesn't update when using ChangeSource() to change beatmap. beatmapOffsetSubscription = realm.SubscribeToPropertyChanged( r => r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings, @@ -105,6 +112,39 @@ protected override void LoadComplete() } } + /// + /// Audio timings in general with newer BASS versions don't match stable. + /// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. + /// + public const double WINDOWS_BASE_AUDIO_OFFSET = 15; + + /// + /// An additional offset applied to account for experimental mode being much better. + /// + public const double WINDOWS_EXPERIMENTAL_AUDIO_OFFSET = -25; + + private void updatePlatformOffset() + { + if (!applyOffsets) + return; + + Debug.Assert(platformOffsetClock != null); + + switch (RuntimeInfo.OS) + { + case RuntimeInfo.Platform.Windows: + platformOffsetClock.Offset = WINDOWS_BASE_AUDIO_OFFSET; + + if (audioManager.UseExperimentalWasapi.Value) + platformOffsetClock.Offset += WINDOWS_EXPERIMENTAL_AUDIO_OFFSET; + return; + + default: + platformOffsetClock.Offset = 0; + break; + } + } + protected override void Update() { base.Update(); diff --git a/osu.Game/Configuration/MigrateNewAudioDialog.cs b/osu.Game/Configuration/MigrateNewAudioDialog.cs new file mode 100644 index 000000000000..f138801a6cb3 --- /dev/null +++ b/osu.Game/Configuration/MigrateNewAudioDialog.cs @@ -0,0 +1,69 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Localisation; +using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; +using osu.Game.Overlays.Settings.Sections.Audio; + +namespace osu.Game.Configuration +{ + public partial class MigrateNewAudioDialog : PopupDialog + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + public MigrateNewAudioDialog(bool wasAlreadyUsing) + { + Icon = FontAwesome.Regular.Bell; + + if (wasAlreadyUsing) + { + HeaderText = @"New audio engine is now default!"; + BodyText = + $""" + We recently added a new "Experimental Audio" backend for Windows users to reduce hitsound latency. Due to overwhelmingly positive feedback, this is now the default mode. + + As you were already using this engine, your audio offset has been adjusted to account for an internal offset change (no intervention required). + + If you have any issues, you can switch back to the legacy engine from settings via the "{AudioSettingsStrings.LegacyAudioLabel}" checkbox. + """; + } + else + { + HeaderText = @"New audio engine has been enabled"; + BodyText = + $""" + We recently added a new "Experimental Audio" backend for Windows users to reduce hitsound latency. Due to overwhelmingly positive feedback, this is now the default mode. + + If you have any issues, you can switch back to the legacy engine below, or at any time in settings via the "{AudioSettingsStrings.LegacyAudioLabel}" checkbox. + """; + + MainContent.Add(new Container + { + Margin = new MarginPadding { Top = 20 }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 400, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new LegacyAudioCheckbox(), + } + }); + } + + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = BeatmapOverlayStrings.UserContentConfirmButtonText, + }, + }; + } + } +} diff --git a/osu.Game/Localisation/AudioSettingsStrings.cs b/osu.Game/Localisation/AudioSettingsStrings.cs index 37ebdd80e062..77912cbb6c09 100644 --- a/osu.Game/Localisation/AudioSettingsStrings.cs +++ b/osu.Game/Localisation/AudioSettingsStrings.cs @@ -100,19 +100,14 @@ public static class AudioSettingsStrings public static LocalisableString AdjustBeatmapOffsetAutomaticallyTooltip => new TranslatableString(getKey(@"adjust_beatmap_offset_automatically_tooltip"), @"If enabled, the offset suggested from last play on a beatmap is automatically applied."); /// - /// "Use experimental audio mode" + /// "Use legacy audio mode" /// - public static LocalisableString WasapiLabel => new TranslatableString(getKey(@"wasapi_label"), @"Use experimental audio mode"); + public static LocalisableString LegacyAudioLabel => new TranslatableString(getKey(@"legacy_audio_label"), @"Use legacy audio mode"); /// - /// "This will attempt to initialise the audio engine in a lower latency mode." + /// "Use this if you are experiencing audio issues. Note that audio latency will be higher when this is toggled on." /// - public static LocalisableString WasapiTooltip => new TranslatableString(getKey(@"wasapi_tooltip"), @"This will attempt to initialise the audio engine in a lower latency mode."); - - /// - /// "Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value." - /// - public static LocalisableString WasapiNotice => new TranslatableString(getKey(@"wasapi_notice"), @"Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value."); + public static LocalisableString LegacyAudioTooltip => new TranslatableString(getKey(@"legacy_audio_tooltip"), @"Use this if you are experiencing audio issues. Note that audio latency will be higher when this is toggled on."); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index b71baaeadad2..aa49a450b231 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -161,6 +161,8 @@ public partial class OsuGame : OsuGameBase, IKeyBindingHandler, IL private OnScreenDisplay onScreenDisplay; + private DialogOverlay dialogOverlay; + [Resolved] private FrameworkConfigManager frameworkConfig { get; set; } @@ -1047,6 +1049,7 @@ protected override IDictionary GetFrameworkConfigDefau { FrameworkSetting.VolumeUniversal, 0.6 }, { FrameworkSetting.VolumeMusic, 0.6 }, { FrameworkSetting.VolumeEffect, 0.6 }, + { FrameworkSetting.AudioUseExperimentalWasapi, true }, }; } @@ -1236,7 +1239,7 @@ protected override void LoadComplete() }, rightFloatingOverlayContent.Add, true); loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true); - loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true); + loadComponentSingleFile(dialogOverlay = new DialogOverlay(), topMostOverlayContent.Add, true); loadComponentSingleFile(new MedalOverlay(), topMostOverlayContent.Add); loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); @@ -1344,6 +1347,21 @@ private void applyConfigMigrations() if (penHandler != null && mouseHandler != null && penHandler.Sensitivity.IsDefault) penHandler.Sensitivity.Value = mouseHandler.Sensitivity.Value; } + + if (combined < 20260521 && RuntimeInfo.OS == RuntimeInfo.Platform.Windows) + { + bool wasAlreadyUsing = Audio.UseExperimentalWasapi.Value; + + // see application of FramedBeatmapClock.WINDOWS_EXPERIMENTAL_AUDIO_OFFSET in FramedBeatmapClock. + // this basically undoes this new offset assuming that users which have been using this setting for a while + // already have had things tuned. + if (wasAlreadyUsing) + LocalConfig.SetValue(OsuSetting.AudioOffset, LocalConfig.Get(OsuSetting.AudioOffset) - FramedBeatmapClock.WINDOWS_EXPERIMENTAL_AUDIO_OFFSET); + + Audio.UseExperimentalWasapi.Value = true; + + dialogOverlay.Push(new MigrateNewAudioDialog(wasAlreadyUsing)); + } } private void handleBackButton() diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 4881c1f1e671..a69995024291 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -40,6 +40,8 @@ public abstract partial class PopupDialog : VisibilityContainer private readonly TextFlowContainer header; private readonly TextFlowContainer body; + public Container MainContent { get; private set; } + private bool actionInvoked; public IconUsage Icon @@ -221,6 +223,13 @@ protected PopupDialog() AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Horizontal = 15 }, }, + MainContent = new Container + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, buttonsContainer = new FillFlowContainer { Anchor = Anchor.TopCentre, diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index 5742272e9676..13019459fdee 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -24,9 +24,7 @@ public partial class AudioDevicesSettings : SettingsSubsection private AudioDeviceDropdown dropdown = null!; - private FormCheckBox? wasapiExperimental; - - private readonly Bindable wasapiExperimentalNote = new Bindable(); + private FormCheckBox? legacyAudio; [BackgroundDependencyLoader] private void load() @@ -44,18 +42,12 @@ private void load() if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) { - Add(new SettingsItemV2(wasapiExperimental = new FormCheckBox - { - Caption = AudioSettingsStrings.WasapiLabel, - HintText = AudioSettingsStrings.WasapiTooltip, - Current = audio.UseExperimentalWasapi, - }) + Add(new SettingsItemV2(legacyAudio = new LegacyAudioCheckbox()) { - Keywords = new[] { "wasapi", "latency", "exclusive" }, - Note = { BindTarget = wasapiExperimentalNote }, + Keywords = new[] { "wasapi", "latency", "exclusive", "legacy", "experimental" }, }); - wasapiExperimental.Current.ValueChanged += _ => onDeviceChanged(string.Empty); + legacyAudio.Current.ValueChanged += _ => onDeviceChanged(string.Empty); } audio.OnNewDevice += onDeviceChanged; @@ -65,18 +57,7 @@ private void load() onDeviceChanged(string.Empty); } - private void onDeviceChanged(string _) - { - updateItems(); - - if (wasapiExperimental != null) - { - if (wasapiExperimental.Current.Value) - wasapiExperimentalNote.Value = new SettingsNote.Data(AudioSettingsStrings.WasapiNotice, SettingsNote.Type.Warning); - else - wasapiExperimentalNote.Value = null; - } - } + private void onDeviceChanged(string _) => Scheduler.AddOnce(updateItems); private void updateItems() { @@ -117,4 +98,37 @@ protected override LocalisableString GenerateItemText(string item) => string.IsNullOrEmpty(item) ? CommonStrings.Default : base.GenerateItemText(item); } } + + public partial class LegacyAudioCheckbox : FormCheckBox + { + private Bindable configExperimentalAudio = null!; + + public LegacyAudioCheckbox() + { + Caption = AudioSettingsStrings.LegacyAudioLabel; + HintText = AudioSettingsStrings.LegacyAudioTooltip; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + configExperimentalAudio = audio.UseExperimentalWasapi.GetBoundCopy(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // Manual two-way binding because we're inverting what the framework exposes. + Current.ValueChanged += legacy => + { + configExperimentalAudio.Value = !legacy.NewValue; + }; + + configExperimentalAudio.BindValueChanged(experimental => + { + Current.Value = !experimental.NewValue; + }, true); + } + } } From f5c70679f5acd6e90d99ec8501c9e171dbc4c90e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 May 2026 10:21:06 +0200 Subject: [PATCH 2/8] Add ability to add videos in editor (#37857) https://github.com/user-attachments/assets/bc329cca-dfa1-4149-9760-121626cf1cb4 --- - Closes https://github.com/ppy/osu/issues/36326 Currently lacks the ability to specify custom video offset that isn't manual .osu editing (defaults to 0) but I'm starting here and listening to requirements. --------- Co-authored-by: Dean Herbert --- .../UserInterface/TestSceneFormControls.cs | 6 + .../UserInterfaceV2/FormFileSelector.cs | 52 +++++- osu.Game/Localisation/EditorSetupStrings.cs | 53 +++--- .../Screens/Edit/Components/FormSampleSet.cs | 2 +- .../Edit/Setup/FormBeatmapFileSelector.cs | 12 +- .../Screens/Edit/Setup/ResourcesSection.cs | 162 +++++++++++++----- ...und.cs => SetupScreenBackgroundPreview.cs} | 4 +- .../Screens/Edit/Setup/SetupScreenHeader.cs | 4 +- .../Edit/Setup/SetupScreenVideoPreview.cs | 115 +++++++++++++ .../Drawables/DrawableStoryboard.cs | 2 +- osu.Game/Storyboards/Storyboard.cs | 2 + 11 files changed, 328 insertions(+), 86 deletions(-) rename osu.Game/Screens/Edit/Setup/{SetupScreenHeaderBackground.cs => SetupScreenBackgroundPreview.cs} (95%) create mode 100644 osu.Game/Screens/Edit/Setup/SetupScreenVideoPreview.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index 22b3753320ca..5d4ef422a495 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -188,6 +188,12 @@ public TestSceneFormControls() Caption = "File selector", PlaceholderText = "Select a file", }, + new FormFileSelector + { + Caption = "File selector with deselection", + PlaceholderText = "Select a file", + AllowClear = true, + }, new FormBeatmapFileSelector(true) { Caption = "File selector with intermediate choice dialog", diff --git a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs index d4edcd2ff377..e6e5686ea8b5 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs @@ -21,9 +21,11 @@ using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; +using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -67,6 +69,12 @@ public Bindable Current /// public LocalisableString PlaceholderText { get; init; } + /// + /// If set to , the selector will display a button, + /// which when clicked, will change 's value to . + /// + public bool AllowClear { get; init; } + public Container PreviewContainer { get; private set; } = null!; private FormControlBackground background = null!; @@ -180,7 +188,7 @@ protected override void LoadComplete() private void onFileSelected() { - if (Current.Value != null) + if (Current.Value != null || AllowClear) this.HidePopover(); initialChooserPath = Current.Value?.DirectoryName; @@ -238,12 +246,12 @@ Task ICanAcceptFiles.Import(params string[] paths) Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException(); - protected virtual FileChooserPopover CreatePopover(string[] handledExtensions, Bindable current, string? chooserPath) => - new FileChooserPopover(handledExtensions, current, chooserPath); + protected virtual FileChooserPopover CreatePopover(string[] handledExtensions, Bindable current, string? chooserPath, bool allowClear) => + new FileChooserPopover(handledExtensions, current, chooserPath, allowClear); public Popover GetPopover() { - var popover = CreatePopover(handledExtensions, Current, initialChooserPath); + var popover = CreatePopover(handledExtensions, Current, initialChooserPath, AllowClear); popoverState.UnbindBindings(); popoverState.BindTo(popover.State); return popover; @@ -258,7 +266,7 @@ public partial class FileChooserPopover : OsuPopover protected OsuFileSelector FileSelector; - public FileChooserPopover(string[] handledExtensions, Bindable current, string? chooserPath) + public FileChooserPopover(string[] handledExtensions, Bindable current, string? chooserPath, bool allowClear) : base(false) { Child = new Container @@ -267,9 +275,37 @@ public FileChooserPopover(string[] handledExtensions, Bindable curren // simplest solution to avoid underlying text to bleed through the bottom border // https://github.com/ppy/osu/pull/30005#issuecomment-2378884430 Padding = new MarginPadding { Bottom = 1 }, - Child = FileSelector = new OsuFileSelector(chooserPath, handledExtensions) + Children = new[] { - RelativeSizeAxes = Axes.Both, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Bottom = allowClear ? 50 : 0 }, + Child = FileSelector = new OsuFileSelector(chooserPath, handledExtensions) + { + RelativeSizeAxes = Axes.Both, + }, + }, + allowClear + ? new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Padding = new MarginPadding(5), + Child = new DangerousRoundedButton + { + Text = CommonStrings.ButtonsClear, + Action = () => OnFileSelected(null), + Enabled = { Value = current.Value != null }, + Padding = new MarginPadding(5), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Width = 60, + } + } + : Empty() }, }; @@ -308,7 +344,7 @@ protected override void LoadComplete() }; } - protected virtual void OnFileSelected(FileInfo file) => current.Value = file; + protected virtual void OnFileSelected(FileInfo? file) => current.Value = file; } } } diff --git a/osu.Game/Localisation/EditorSetupStrings.cs b/osu.Game/Localisation/EditorSetupStrings.cs index 66bafcc445b2..9d41b2cea9db 100644 --- a/osu.Game/Localisation/EditorSetupStrings.cs +++ b/osu.Game/Localisation/EditorSetupStrings.cs @@ -42,8 +42,7 @@ public static class EditorSetupStrings /// /// "If enabled, an "Are you ready? 3, 2, 1, GO!" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so." /// - public static LocalisableString CountdownDescription => new TranslatableString(getKey(@"countdown_description"), - @"If enabled, an ""Are you ready? 3, 2, 1, GO!"" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so."); + public static LocalisableString CountdownDescription => new TranslatableString(getKey(@"countdown_description"), @"If enabled, an ""Are you ready? 3, 2, 1, GO!"" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so."); /// /// "Countdown speed" @@ -53,8 +52,7 @@ public static class EditorSetupStrings /// /// "If the countdown sounds off-time, use this to make it appear one or more beats early." /// - public static LocalisableString CountdownOffsetDescription => - new TranslatableString(getKey(@"countdown_offset_description"), @"If the countdown sounds off-time, use this to make it appear one or more beats early."); + public static LocalisableString CountdownOffsetDescription => new TranslatableString(getKey(@"countdown_offset_description"), @"If the countdown sounds off-time, use this to make it appear one or more beats early."); /// /// "Countdown offset" @@ -69,8 +67,7 @@ public static class EditorSetupStrings /// /// "Allows storyboards to use the full screen space, rather than be confined to a 4:3 area." /// - public static LocalisableString WidescreenSupportDescription => - new TranslatableString(getKey(@"widescreen_support_description"), @"Allows storyboards to use the full screen space, rather than be confined to a 4:3 area."); + public static LocalisableString WidescreenSupportDescription => new TranslatableString(getKey(@"widescreen_support_description"), @"Allows storyboards to use the full screen space, rather than be confined to a 4:3 area."); /// /// "Epilepsy warning" @@ -80,8 +77,7 @@ public static class EditorSetupStrings /// /// "Recommended if the storyboard or video contain scenes with rapidly flashing colours." /// - public static LocalisableString EpilepsyWarningDescription => - new TranslatableString(getKey(@"epilepsy_warning_description"), @"Recommended if the storyboard or video contain scenes with rapidly flashing colours."); + public static LocalisableString EpilepsyWarningDescription => new TranslatableString(getKey(@"epilepsy_warning_description"), @"Recommended if the storyboard or video contain scenes with rapidly flashing colours."); /// /// "Letterbox during breaks" @@ -91,8 +87,7 @@ public static class EditorSetupStrings /// /// "Adds horizontal letterboxing to give a cinematic look during breaks." /// - public static LocalisableString LetterboxDuringBreaksDescription => - new TranslatableString(getKey(@"letterbox_during_breaks_description"), @"Adds horizontal letterboxing to give a cinematic look during breaks."); + public static LocalisableString LetterboxDuringBreaksDescription => new TranslatableString(getKey(@"letterbox_during_breaks_description"), @"Adds horizontal letterboxing to give a cinematic look during breaks."); /// /// "Samples match playback rate" @@ -102,8 +97,7 @@ public static class EditorSetupStrings /// /// "When enabled, all samples will speed up or slow down when rate-changing mods are enabled." /// - public static LocalisableString SamplesMatchPlaybackRateDescription => new TranslatableString(getKey(@"samples_match_playback_rate_description"), - @"When enabled, all samples will speed up or slow down when rate-changing mods are enabled."); + public static LocalisableString SamplesMatchPlaybackRateDescription => new TranslatableString(getKey(@"samples_match_playback_rate_description"), @"When enabled, all samples will speed up or slow down when rate-changing mods are enabled."); /// /// "The size of all hit objects" @@ -123,8 +117,7 @@ public static class EditorSetupStrings /// /// "The harshness of hit windows and difficulty of special objects (ie. spinners)" /// - public static LocalisableString OverallDifficultyDescription => - new TranslatableString(getKey(@"overall_difficulty_description"), @"The harshness of hit windows and difficulty of special objects (ie. spinners)"); + public static LocalisableString OverallDifficultyDescription => new TranslatableString(getKey(@"overall_difficulty_description"), @"The harshness of hit windows and difficulty of special objects (ie. spinners)"); /// /// "Tick Rate" @@ -134,8 +127,7 @@ public static class EditorSetupStrings /// /// "Determines how many "ticks" are generated within long hit objects. A tick rate of 1 will generate ticks on each beat, 2 would be twice per beat, etc." /// - public static LocalisableString TickRateDescription => new TranslatableString(getKey(@"tick_rate_description"), - @"Determines how many ""ticks"" are generated within long hit objects. A tick rate of 1 will generate ticks on each beat, 2 would be twice per beat, etc."); + public static LocalisableString TickRateDescription => new TranslatableString(getKey(@"tick_rate_description"), @"Determines how many ""ticks"" are generated within long hit objects. A tick rate of 1 will generate ticks on each beat, 2 would be twice per beat, etc."); /// /// "Base Velocity" @@ -145,8 +137,7 @@ public static class EditorSetupStrings /// /// "The base velocity of the beatmap, affecting things like slider velocity and scroll speed in some rulesets." /// - public static LocalisableString BaseVelocityDescription => new TranslatableString(getKey(@"base_velocity_description"), - @"The base velocity of the beatmap, affecting things like slider velocity and scroll speed in some rulesets."); + public static LocalisableString BaseVelocityDescription => new TranslatableString(getKey(@"base_velocity_description"), @"The base velocity of the beatmap, affecting things like slider velocity and scroll speed in some rulesets."); /// /// "Metadata" @@ -188,6 +179,16 @@ public static class EditorSetupStrings /// public static LocalisableString AudioTrack => new TranslatableString(getKey(@"audio_track"), @"Audio Track"); + /// + /// "Video" + /// + public static LocalisableString Video => new TranslatableString(getKey(@"video"), @"Video"); + + /// + /// "The video will be used instead of the static background, if present. Beatmap downloads are offered both with and without video, so if adding a video, a matching background should also be provided." + /// + public static LocalisableString VideoHint => new TranslatableString(getKey(@"video_hint"), @"The video will be used instead of the static background, if present. Beatmap downloads are offered both with and without video, so if adding a video, a matching background should also be provided."); + /// /// "Custom sample sets" /// @@ -198,6 +199,11 @@ public static class EditorSetupStrings /// public static LocalisableString ClickToSelectTrack => new TranslatableString(getKey(@"click_to_select_track"), @"Click to select a track"); + /// + /// "Click to select a video" + /// + public static LocalisableString ClickToSelectVideo => new TranslatableString(getKey(@"click_to_select_video"), @"Click to select a video"); + /// /// "Click to select a background image" /// @@ -221,14 +227,12 @@ public static class EditorSetupStrings /// /// "Sync metadata with all difficulties" /// - public static LocalisableString SyncMetadataWithAllDifficulties => - new TranslatableString(getKey(@"sync_metadata_with_all_difficulties"), @"Sync metadata with all difficulties"); + public static LocalisableString SyncMetadataWithAllDifficulties => new TranslatableString(getKey(@"sync_metadata_with_all_difficulties"), @"Sync metadata with all difficulties"); /// /// "Copies artist, title, source, and tags to all difficulties." /// - public static LocalisableString SyncMetadataWithAllDifficultiesTooltip => new TranslatableString(getKey(@"sync_metadata_with_all_difficulties_tooltip"), - @"Copies artist, title, source, and tags to all difficulties."); + public static LocalisableString SyncMetadataWithAllDifficultiesTooltip => new TranslatableString(getKey(@"sync_metadata_with_all_difficulties_tooltip"), @"Copies artist, title, source, and tags to all difficulties."); /// /// "Ruleset ({0})" @@ -260,6 +264,11 @@ public static class EditorSetupStrings /// public static LocalisableString DragToSetBackground => new TranslatableString(getKey(@"drag_to_set_background"), @"Drag image here to set beatmap background!"); + /// + /// "Drag video here to set beatmap video!" + /// + public static LocalisableString DragToSetVideo => new TranslatableString(getKey(@"drag_to_set_video"), @"Drag video here to set beatmap video!"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Edit/Components/FormSampleSet.cs b/osu.Game/Screens/Edit/Components/FormSampleSet.cs index 370d36dd8cd9..bbe7a550307a 100644 --- a/osu.Game/Screens/Edit/Components/FormSampleSet.cs +++ b/osu.Game/Screens/Edit/Components/FormSampleSet.cs @@ -329,7 +329,7 @@ private void deleteSample() } public Popover? GetPopover() => ActualFilename.Value == null - ? new FormFileSelector.FileChooserPopover(SupportedExtensions.AUDIO_EXTENSIONS, selectedFile, LastSelectedFileDirectory.Value?.FullName) + ? new FormFileSelector.FileChooserPopover(SupportedExtensions.AUDIO_EXTENSIONS, selectedFile, LastSelectedFileDirectory.Value?.FullName, allowClear: false) : null; public MenuItem[]? ContextMenuItems => diff --git a/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs b/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs index 53287383ec2a..db66be0f4b4b 100644 --- a/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs +++ b/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Diagnostics; using System.IO; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -38,9 +37,9 @@ public FormBeatmapFileSelector(bool beatmapHasMultipleDifficulties, params strin this.beatmapHasMultipleDifficulties = beatmapHasMultipleDifficulties; } - protected override FileChooserPopover CreatePopover(string[] handledExtensions, Bindable current, string? chooserPath) + protected override FileChooserPopover CreatePopover(string[] handledExtensions, Bindable current, string? chooserPath, bool allowClear) { - var popover = new BeatmapFileChooserPopover(handledExtensions, current, chooserPath, beatmapHasMultipleDifficulties); + var popover = new BeatmapFileChooserPopover(handledExtensions, current, chooserPath, allowClear, beatmapHasMultipleDifficulties); popover.ApplyToAllDifficulties.BindTo(ApplyToAllDifficulties); return popover; } @@ -53,8 +52,8 @@ private partial class BeatmapFileChooserPopover : FileChooserPopover private Container selectApplicationScopeContainer = null!; - public BeatmapFileChooserPopover(string[] handledExtensions, Bindable current, string? chooserPath, bool beatmapHasMultipleDifficulties) - : base(handledExtensions, current, chooserPath) + public BeatmapFileChooserPopover(string[] handledExtensions, Bindable current, string? chooserPath, bool allowClear, bool beatmapHasMultipleDifficulties) + : base(handledExtensions, current, chooserPath, allowClear) { this.beatmapHasMultipleDifficulties = beatmapHasMultipleDifficulties; } @@ -137,7 +136,7 @@ private void load(OverlayColourProvider colourProvider, OsuColour colours) }); } - protected override void OnFileSelected(FileInfo file) + protected override void OnFileSelected(FileInfo? file) { if (beatmapHasMultipleDifficulties) selectApplicationScopeContainer.FadeIn(200, Easing.InQuint); @@ -147,7 +146,6 @@ protected override void OnFileSelected(FileInfo file) private void updateFileSelection() { - Debug.Assert(FileSelector.CurrentFile.Value != null); base.OnFileSelected(FileSelector.CurrentFile.Value); } } diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 31b64e53a9c9..f7945f2754e6 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Framework.Logging; @@ -15,6 +16,7 @@ using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit.Components; +using osu.Game.Storyboards; using osu.Game.Utils; namespace osu.Game.Screens.Edit.Setup @@ -23,6 +25,7 @@ public partial class ResourcesSection : SetupSection { private FormBeatmapFileSelector audioTrackChooser = null!; private FormBeatmapFileSelector backgroundChooser = null!; + private FormBeatmapFileSelector videoChooser = null!; private readonly Bindable currentSampleSet = new Bindable(); @@ -35,7 +38,7 @@ public partial class ResourcesSection : SetupSection private BeatmapManager beatmaps { get; set; } = null!; [Resolved] - private IBindable working { get; set; } = null!; + private IBindable currentWorkingBeatmap { get; set; } = null!; [Resolved] private Editor? editor { get; set; } @@ -43,18 +46,24 @@ public partial class ResourcesSection : SetupSection [Resolved] private SetupScreen setupScreen { get; set; } = null!; - private SetupScreenHeaderBackground headerBackground = null!; + private SetupScreenBackgroundPreview backgroundPreview = null!; + private SetupScreenVideoPreview videoPreview = null!; [BackgroundDependencyLoader] private void load() { - headerBackground = new SetupScreenHeaderBackground + backgroundPreview = new SetupScreenBackgroundPreview + { + RelativeSizeAxes = Axes.X, + Height = 110, + }; + videoPreview = new SetupScreenVideoPreview { RelativeSizeAxes = Axes.X, Height = 110, }; - bool beatmapHasMultipleDifficulties = working.Value.BeatmapSetInfo.Beatmaps.Count > 1; + bool beatmapHasMultipleDifficulties = currentWorkingBeatmap.Value.BeatmapSetInfo.Beatmaps.Count > 1; Children = new Drawable[] { @@ -63,6 +72,13 @@ private void load() Caption = GameplaySettingsStrings.BackgroundHeader, PlaceholderText = EditorSetupStrings.ClickToSelectBackground, }, + videoChooser = new FormBeatmapFileSelector(beatmapHasMultipleDifficulties, SupportedExtensions.VIDEO_EXTENSIONS) + { + Caption = EditorSetupStrings.Video, + PlaceholderText = EditorSetupStrings.ClickToSelectVideo, + HintText = EditorSetupStrings.VideoHint, + AllowClear = true, + }, audioTrackChooser = new FormBeatmapFileSelector(beatmapHasMultipleDifficulties, SupportedExtensions.AUDIO_EXTENSIONS) { Caption = EditorSetupStrings.AudioTrack, @@ -79,27 +95,32 @@ private void load() { string actualFilename = string.Concat(targetName, file.Extension); using var stream = file.OpenRead(); - beatmaps.AddFile(working.Value.BeatmapSetInfo, stream, actualFilename); + beatmaps.AddFile(currentWorkingBeatmap.Value.BeatmapSetInfo, stream, actualFilename); return actualFilename; }, SampleRemoveRequested = filename => { - var file = working.Value.BeatmapSetInfo.GetFile(filename); + var file = currentWorkingBeatmap.Value.BeatmapSetInfo.GetFile(filename); if (file != null) - beatmaps.DeleteFile(working.Value.BeatmapSetInfo, file); + beatmaps.DeleteFile(currentWorkingBeatmap.Value.BeatmapSetInfo, file); } }, }; - backgroundChooser.PreviewContainer.Add(headerBackground); + backgroundChooser.PreviewContainer.Add(backgroundPreview); + videoChooser.PreviewContainer.Add(videoPreview); + + if (!string.IsNullOrEmpty(currentWorkingBeatmap.Value.Metadata.BackgroundFile)) + backgroundChooser.Current.Value = new FileInfo(currentWorkingBeatmap.Value.Metadata.BackgroundFile); - if (!string.IsNullOrEmpty(working.Value.Metadata.BackgroundFile)) - backgroundChooser.Current.Value = new FileInfo(working.Value.Metadata.BackgroundFile); + if (currentWorkingBeatmap.Value.Storyboard.PrimaryVideo is StoryboardVideo video) + videoChooser.Current.Value = new FileInfo(video.Path); - if (!string.IsNullOrEmpty(working.Value.Metadata.AudioFile)) - audioTrackChooser.Current.Value = new FileInfo(working.Value.Metadata.AudioFile); + if (!string.IsNullOrEmpty(currentWorkingBeatmap.Value.Metadata.AudioFile)) + audioTrackChooser.Current.Value = new FileInfo(currentWorkingBeatmap.Value.Metadata.AudioFile); backgroundChooser.Current.BindValueChanged(backgroundChanged); + videoChooser.Current.BindValueChanged(videoChanged); audioTrackChooser.Current.BindValueChanged(audioTrackChanged); } @@ -109,10 +130,30 @@ public bool ChangeBackgroundImage(FileInfo source, bool applyToAllDifficulties) return false; changeResource(source, applyToAllDifficulties, @"bg", - metadata => metadata.BackgroundFile, - (metadata, name) => metadata.BackgroundFile = name); + working => working.BeatmapInfo.Metadata.BackgroundFile, + (working, name) => working.BeatmapInfo.Metadata.BackgroundFile = name.AsNonNull()); + + backgroundPreview.UpdateBackground(); + editor?.ApplyToBackground(bg => ((EditorBackgroundScreen)bg).RefreshBackgroundAsync()); + return true; + } + + public bool ChangeVideo(FileInfo? source, bool applyToAllDifficulties) + { + if (source != null && !source.Exists) + return false; - headerBackground.UpdateBackground(); + changeResource(source, applyToAllDifficulties, @"video", + working => working.Storyboard.PrimaryVideo?.Path ?? string.Empty, + (working, name) => + { + var videoLayer = working.Storyboard.GetLayer(@"Video"); + videoLayer.Elements.RemoveAll(elem => elem is StoryboardVideo); + if (name != null) + videoLayer.Elements.Insert(0, new StoryboardVideo(StoryboardElementSource.Beatmap, name, 0)); + }); + + videoPreview.UpdateVideo(); editor?.ApplyToBackground(bg => ((EditorBackgroundScreen)bg).RefreshBackgroundAsync()); return true; } @@ -140,21 +181,21 @@ public bool ChangeAudioTrack(FileInfo source, bool applyToAllDifficulties) } changeResource(source, applyToAllDifficulties, @"audio", - metadata => metadata.AudioFile, - (metadata, name) => + working => working.BeatmapInfo.Metadata.AudioFile, + (working, name) => { - metadata.AudioFile = name; + working.BeatmapInfo.Metadata.AudioFile = name.AsNonNull(); if (!string.IsNullOrWhiteSpace(artist)) { - metadata.ArtistUnicode = artist; - metadata.Artist = MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode); + working.BeatmapInfo.Metadata.ArtistUnicode = artist; + working.BeatmapInfo.Metadata.Artist = MetadataUtils.StripNonRomanisedCharacters(working.BeatmapInfo.Metadata.ArtistUnicode); } if (!string.IsNullOrEmpty(title)) { - metadata.TitleUnicode = title; - metadata.Title = MetadataUtils.StripNonRomanisedCharacters(metadata.TitleUnicode); + working.BeatmapInfo.Metadata.TitleUnicode = title; + working.BeatmapInfo.Metadata.Title = MetadataUtils.StripNonRomanisedCharacters(working.BeatmapInfo.Metadata.TitleUnicode); } }); @@ -163,65 +204,86 @@ public bool ChangeAudioTrack(FileInfo source, bool applyToAllDifficulties) return true; } - private void changeResource(FileInfo source, bool applyToAllDifficulties, string baseFilename, Func readFilename, Action writeMetadata) + private void changeResource( + FileInfo? source, + bool applyToAllDifficulties, + string baseFilename, + Func readOldFilenameFrom, + Action writeNewFilenameTo) { - var set = working.Value.BeatmapSetInfo; - var beatmap = working.Value.BeatmapInfo; + var set = currentWorkingBeatmap.Value.BeatmapSetInfo; + var currentBeatmapInfo = currentWorkingBeatmap.Value.BeatmapInfo; - var otherBeatmaps = set.Beatmaps.Where(b => !b.Equals(beatmap)); + var otherBeatmaps = set.Beatmaps.Where(b => !b.Equals(currentBeatmapInfo)); // First, clean up files which will no longer be used. if (applyToAllDifficulties) { foreach (var b in set.Beatmaps) { - if (set.GetFile(readFilename(b.Metadata)) is RealmNamedFileUsage otherExistingFile) + var working = beatmaps.GetWorkingBeatmap(b); + if (set.GetFile(readOldFilenameFrom(working)) is RealmNamedFileUsage otherExistingFile) beatmaps.DeleteFile(set, otherExistingFile); } } else { - RealmNamedFileUsage? oldFile = set.GetFile(readFilename(working.Value.Metadata)); + RealmNamedFileUsage? oldFile = set.GetFile(readOldFilenameFrom(currentWorkingBeatmap.Value)); if (oldFile != null) { - bool oldFileUsedInOtherDiff = otherBeatmaps - .Any(b => readFilename(b.Metadata) == oldFile.Filename); + bool oldFileUsedInOtherDiff = false; + + foreach (var b in otherBeatmaps) + { + var working = beatmaps.GetWorkingBeatmap(b); + + if (readOldFilenameFrom(working) == oldFile.Filename) + { + oldFileUsedInOtherDiff = true; + break; + } + } + if (!oldFileUsedInOtherDiff) beatmaps.DeleteFile(set, oldFile); } } - // Choose a new filename that doesn't clash with any other existing files. - string newFilename = $"{baseFilename}{source.Extension}"; + string? newFilename = null; - if (set.GetFile(newFilename) != null) + if (source != null) { - string[] existingFilenames = set.Files.Select(f => f.Filename).Where(f => - f.StartsWith(baseFilename, StringComparison.OrdinalIgnoreCase) && - f.EndsWith(source.Extension, StringComparison.OrdinalIgnoreCase)).ToArray(); - newFilename = NamingUtils.GetNextBestFilename(existingFilenames, $@"{baseFilename}{source.Extension}"); - } + // Choose a new filename that doesn't clash with any other existing files. + newFilename = $"{baseFilename}{source.Extension}"; - using (var stream = source.OpenRead()) - beatmaps.AddFile(set, stream, newFilename); + if (set.GetFile(newFilename) != null) + { + string[] existingFilenames = set.Files.Select(f => f.Filename).Where(f => + f.StartsWith(baseFilename, StringComparison.OrdinalIgnoreCase) && + f.EndsWith(source.Extension, StringComparison.OrdinalIgnoreCase)).ToArray(); + newFilename = NamingUtils.GetNextBestFilename(existingFilenames, $@"{baseFilename}{source.Extension}"); + } + + using (var stream = source.OpenRead()) + beatmaps.AddFile(set, stream, newFilename); + } if (applyToAllDifficulties) { foreach (var b in otherBeatmaps) { - writeMetadata(b.Metadata, newFilename); - // save the difficulty to re-encode the .osu file, updating any reference of the old filename. // // note that this triggers a full save flow, including triggering a difficulty calculation. // this is not a cheap operation and should be reconsidered in the future. var beatmapWorking = beatmaps.GetWorkingBeatmap(b); + writeNewFilenameTo(beatmapWorking, newFilename); beatmaps.Save(b, beatmapWorking.GetPlayableBeatmap(b.Ruleset), beatmapWorking.GetSkin(), beatmapWorking.Storyboard); } } - writeMetadata(beatmap.Metadata, newFilename); + writeNewFilenameTo(currentWorkingBeatmap.Value, newFilename); // editor change handler cannot be aware of any file changes or other difficulties having their metadata modified. // for simplicity's sake, trigger a save when changing any resource to ensure the change is correctly saved. @@ -236,6 +298,7 @@ private void changeResource(FileInfo source, bool applyToAllDifficulties, string // note that this means that `Change{BackgroundImage,AudioTrack}()` are required to not have made any modifications to the beatmap files // (or at least cleaned them up properly themselves) if they return `false`. private bool rollingBackBackgroundChange; + private bool rollingBackVideoChange; private bool rollingBackAudioChange; private void backgroundChanged(ValueChangedEvent file) @@ -251,6 +314,19 @@ private void backgroundChanged(ValueChangedEvent file) } } + private void videoChanged(ValueChangedEvent file) + { + if (rollingBackVideoChange) + return; + + if (!ChangeVideo(file.NewValue, videoChooser.ApplyToAllDifficulties.Value)) + { + rollingBackVideoChange = true; + videoChooser.Current.Value = file.OldValue; + rollingBackVideoChange = false; + } + } + private void audioTrackChanged(ValueChangedEvent file) { if (rollingBackAudioChange) diff --git a/osu.Game/Screens/Edit/Setup/SetupScreenHeaderBackground.cs b/osu.Game/Screens/Edit/Setup/SetupScreenBackgroundPreview.cs similarity index 95% rename from osu.Game/Screens/Edit/Setup/SetupScreenHeaderBackground.cs rename to osu.Game/Screens/Edit/Setup/SetupScreenBackgroundPreview.cs index 5f3e6eb46950..8965d22e09a5 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreenHeaderBackground.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreenBackgroundPreview.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Edit.Setup { - public partial class SetupScreenHeaderBackground : CompositeDrawable + public partial class SetupScreenBackgroundPreview : CompositeDrawable { [Resolved] private OsuColour colours { get; set; } = null!; @@ -24,7 +24,7 @@ public partial class SetupScreenHeaderBackground : CompositeDrawable private readonly Container content; - public SetupScreenHeaderBackground() + public SetupScreenBackgroundPreview() { InternalChild = content = new Container { diff --git a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs index 022da36abc8e..a7d25725d1c3 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Edit.Setup { internal partial class SetupScreenHeader : OverlayHeader { - public SetupScreenHeaderBackground Background { get; private set; } = null!; + public SetupScreenBackgroundPreview BackgroundPreview { get; private set; } = null!; [Resolved] private SectionsContainer sections { get; set; } = null!; @@ -43,7 +43,7 @@ internal partial class SetupScreenHeader : OverlayHeader RelativeSizeAxes = Axes.X, Height = 30 }, - Background = new SetupScreenHeaderBackground + BackgroundPreview = new SetupScreenBackgroundPreview { RelativeSizeAxes = Axes.X, Height = 120 diff --git a/osu.Game/Screens/Edit/Setup/SetupScreenVideoPreview.cs b/osu.Game/Screens/Edit/Setup/SetupScreenVideoPreview.cs new file mode 100644 index 000000000000..e852852200c3 --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/SetupScreenVideoPreview.cs @@ -0,0 +1,115 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.Video; +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Localisation; +using osu.Game.Storyboards.Drawables; + +namespace osu.Game.Screens.Edit.Setup +{ + public partial class SetupScreenVideoPreview : CompositeDrawable + { + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private IBindable working { get; set; } = null!; + + private DependencyContainer dependencies = null!; + private TextureStore textureStore = null!; + private readonly Container content; + + public SetupScreenVideoPreview() + { + InternalChild = content = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 3.5f, + }; + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => + dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + [BackgroundDependencyLoader] + private void load(GameHost host, RealmAccess realmAccess) + { + var lookupStore = new DrawableStoryboard.StoryboardResourceLookupStore(working.Value.Storyboard, realmAccess, host); + dependencies.CacheAs(textureStore = new TextureStore(host.Renderer, host.CreateTextureLoaderStore(lookupStore), false, scaleAdjust: 1)); + + UpdateVideo(); + } + + public void UpdateVideo() + { + var video = working.Value.Storyboard.PrimaryVideo; + + if (video == null) + { + displayPlaceholder(); + return; + } + + var stream = textureStore.GetStream(video.Path); + + if (stream == null) + { + displayPlaceholder(); + return; + } + + LoadComponentAsync(new Video(stream, startAtCurrentTime: false) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + Loop = true, + }, v => + { + content.Child = v; + v.FadeInFromZero(500); + }); + } + + private void displayPlaceholder() + { + content.Children = new Drawable[] + { + new Box + { + Colour = colours.GreySeaFoamDarker, + RelativeSizeAxes = Axes.Both, + }, + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 24)) + { + Text = EditorSetupStrings.DragToSetVideo, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both + } + }; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (textureStore.IsNotNull()) + textureStore.Dispose(); + } + } +} diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 5ef6b30a8281..0475487d6c81 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -127,7 +127,7 @@ private void updateLayerVisibility() layer.Enabled = passing.Value ? layer.Layer.VisibleWhenPassing : layer.Layer.VisibleWhenFailing; } - private class StoryboardResourceLookupStore : IResourceStore + public class StoryboardResourceLookupStore : IResourceStore { private readonly IResourceStore realmFileStore; private readonly Storyboard storyboard; diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 4cad1ba4adb0..4dc96c84a476 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -53,6 +53,8 @@ public class Storyboard .Where(e => e is not StoryboardVideo) .MaxBy(e => e.GetEndTime())?.GetEndTime(); + public StoryboardVideo? PrimaryVideo => GetLayer(@"Video").Elements.OfType().FirstOrDefault(); + /// /// Depth of the currently front-most storyboard layer, excluding the overlay layer. /// From 21938eea960d357ae87de3de3a347a55a423ef51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 May 2026 12:22:50 +0200 Subject: [PATCH 3/8] Handle background offset when encoding/decoding beatmaps (#37841) - Part of https://github.com/ppy/osu/issues/14238 (doesn't close, because the property doesn't do anything yet). - Supersedes / closes https://github.com/ppy/osu/pull/37467. --- .../Formats/LegacyStoryboardEncoderTest.cs | 17 +++++++++++++++++ .../Beatmaps/Formats/LegacyStoryboardDecoder.cs | 14 ++++++++++++++ .../Beatmaps/Formats/LegacyStoryboardEncoder.cs | 3 +-- osu.Game/Storyboards/Storyboard.cs | 7 +++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardEncoderTest.cs index ad6d0fb7c377..1b340ddd24a9 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardEncoderTest.cs @@ -30,6 +30,23 @@ public void TestBackground() Assert.That(decodedAfterEncode.Beatmap.BeatmapInfo.Metadata.BackgroundFile, Is.EqualTo("bg.jpg")); } + [Test] + public void TestBackgroundOffset() + { + var initial = createComponents(); + initial.Beatmap.BeatmapInfo.Metadata.BackgroundFile = "bg_offset.jpg"; + initial.Storyboard.BackgroundOffset = new Vector2(0, 45); + + var encoded = encode(initial); + var decodedAfterEncode = decode(encoded); + + Assert.Multiple(() => + { + Assert.That(decodedAfterEncode.Beatmap.BeatmapInfo.Metadata.BackgroundFile, Is.EqualTo("bg_offset.jpg")); + Assert.That(decodedAfterEncode.Storyboard.BackgroundOffset, Is.EqualTo(new Vector2(0, 45))); + }); + } + [Test] public void TestVideos() { diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index eb1dacb4477a..563ef7836218 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -120,6 +120,20 @@ private void handleEvents(string line, bool isPrimaryStream) switch (type) { + case LegacyEventType.Background: + { + // the actual filename is handled in `LegacyBeatmapDecoder`. + // this only handles the background offset, because it does not logically belong in `Beatmap` or related classes. + if (split.Length > 4) + { + float x = Parsing.ParseFloat(split[3]); + float y = Parsing.ParseFloat(split[4]); + storyboard.BackgroundOffset = new Vector2(x, y); + } + + break; + } + case LegacyEventType.Video: { int offset = Parsing.ParseInt(split[1]); diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardEncoder.cs index 7238f2e7afb5..09148899e72c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardEncoder.cs @@ -54,10 +54,9 @@ private void encodeEvents(TextWriter writer, StoryboardElementSource target) if (target == StoryboardElementSource.Beatmap) { // https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1499 - // TODO: handle nonzero background offset (https://github.com/ppy/osu/issues/14238) writer.WriteLine(string.Format(CultureInfo.InvariantCulture, @"{0},{1},""{2}"",{3},{4}", - (int)LegacyEventType.Background, 0, storyboard.BeatmapInfo.Metadata.BackgroundFile, 0, 0)); + (int)LegacyEventType.Background, 0, storyboard.BeatmapInfo.Metadata.BackgroundFile, storyboard.BackgroundOffset.X, storyboard.BackgroundOffset.Y)); } // https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1496 diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 4dc96c84a476..d3c10aa162c4 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Storyboards.Drawables; using osu.Game.Utils; +using osuTK; namespace osu.Game.Storyboards { @@ -98,6 +99,12 @@ public bool ReplacesBackground } } + /// + /// Offset to be applied to the beatmap background. + /// TODO: Unused yet. See https://github.com/ppy/osu/issues/14238. + /// + public Vector2 BackgroundOffset { get; set; } = Vector2.Zero; + public virtual DrawableStoryboard CreateDrawable(IReadOnlyList? mods = null) => new DrawableStoryboard(this, mods); From bf60ef163361bc2706a6962118f7c9fa58e91c63 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2026 19:29:51 +0900 Subject: [PATCH 4/8] Increase minimum size of video/storyboard icons globally (#37866) They were shockingly small. | Before | After | | :---: | :---: | | osu! 2026-05-22 at 08 19 27 | osu! 2026-05-22 at 08 17 23 | | osu! 2026-05-22 at 08 19 34 | osu! 2026-05-22 at 08 17 16 | --- osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs | 4 ++-- osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs | 4 ++-- .../MatchmakingSelectPanel.CardContentBeatmap.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index 4c4a0637084f..26553ac2e0e9 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -244,10 +244,10 @@ private void load(BeatmapSetOverlay? beatmapSetOverlay) }); if (BeatmapSet.HasVideo) - leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) }); + leftIconArea.Add(new VideoIconPill()); if (BeatmapSet.HasStoryboard) - leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) }); + leftIconArea.Add(new StoryboardIconPill()); if (BeatmapSet.FeaturedInSpotlight) { diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs index 6974cf81ea92..3e0d61bcb904 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs @@ -226,10 +226,10 @@ private void load() }); if (BeatmapSet.HasVideo) - leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) }); + leftIconArea.Add(new VideoIconPill()); if (BeatmapSet.HasStoryboard) - leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) }); + leftIconArea.Add(new StoryboardIconPill()); if (BeatmapSet.FeaturedInSpotlight) { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs index e2d5fa7890bf..ede68cfa7e72 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs @@ -307,10 +307,10 @@ private void load(OsuColour colours) }; if (beatmapSet.HasVideo) - leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) }); + leftIconArea.Add(new VideoIconPill()); if (beatmapSet.HasStoryboard) - leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) }); + leftIconArea.Add(new StoryboardIconPill()); if (beatmapSet.HasExplicitContent) { From 6ccef8736c8eca923885be52818d1ab367145fd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2026 21:15:11 +0900 Subject: [PATCH 5/8] placeholder From 43b44109d34187c0cf51ac0d591d85bafe184d9b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 May 2026 13:54:48 +0000 Subject: [PATCH 6/8] Fix CI CS0246 in Storyboard by switching to System.Numerics Vector2 Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/2d91ee13-1e66-4df6-8db3-cf2a32a6b15e Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- osu.Game/Storyboards/Storyboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index d3c10aa162c4..278fda3ce031 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -5,11 +5,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Numerics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Storyboards.Drawables; using osu.Game.Utils; -using osuTK; namespace osu.Game.Storyboards { From c90f5ab2d44ab27f8bd698890cfbf7d8912a1f0a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 05:23:08 +0000 Subject: [PATCH 7/8] fix: add missing CommonStrings namespace import Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/e6082aeb-9082-4038-8ea9-4bd898edac78 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs index 0353b9dac721..ede1b91d3131 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs @@ -22,6 +22,7 @@ using osu.Game.Database; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Overlays; using System.Numerics; From 94deee56091131589dabbb6a3e7e259602a7df5c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 05:45:26 +0000 Subject: [PATCH 8/8] fix: use web CommonStrings in FormFileSelector Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/ab2ee4ea-fd9a-4375-ae5f-14317e61a903 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs index ede1b91d3131..5b3bc41b4285 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs @@ -22,8 +22,8 @@ using osu.Game.Database; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Localisation; using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; using System.Numerics; namespace osu.Game.Graphics.UserInterfaceV2