Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow choosing different background/audio files for individual difficulties #30860

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
259ad8a
Add failing test cases
frenzibyte Nov 24, 2024
871c365
Preserve existing beatmap background/audio files if used elsewhere
frenzibyte Nov 24, 2024
8e20dc7
Add option to update all difficulties with new background/audio file
frenzibyte Nov 24, 2024
e348b3a
Only enable button if there are multiple difficulties
frenzibyte Nov 24, 2024
dc210d5
Add test coverage for sync button
frenzibyte Nov 24, 2024
c8b13b7
Add localisation support
frenzibyte Nov 24, 2024
a872f74
Make sync button only affect changed resource type
frenzibyte Nov 24, 2024
95a6226
Only enable button if there are different filenames
frenzibyte Nov 24, 2024
3480da2
Remove no-op `SaveState` call
frenzibyte Nov 24, 2024
1468385
Reset new file states after syncing
frenzibyte Nov 25, 2024
b70fb4b
Add `FormBeatmapFileSelector` for intermediate user-choice step
frenzibyte Nov 27, 2024
efb68e4
Refactor `ResourcesSection` to support new form of selection
frenzibyte Nov 27, 2024
4b8094d
Update test coverage
frenzibyte Nov 27, 2024
238a1ce
Fix tests reliability and improve code
frenzibyte Nov 27, 2024
4d9d5ad
Rename parameter to be more clear
peppy Nov 28, 2024
32b34c1
Rename container to make more sense
peppy Nov 28, 2024
4a1401a
Rewrite bindable flow to make more sense
frenzibyte Nov 28, 2024
b1d0939
Add localisation support
frenzibyte Nov 28, 2024
311f094
Abstractify resource change logic and share between background and audio
frenzibyte Nov 28, 2024
489d7a3
Perform a single `Save` call rather than doing it in each difficulty
frenzibyte Nov 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 180 additions & 28 deletions osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,35 +98,9 @@ public void TestAddAudioTrack()
AddStep("enter compose mode", () => InputManager.Key(Key.F1));
AddUntilStep("wait for timeline load", () => Editor.ChildrenOfType<Timeline>().FirstOrDefault()?.IsLoaded == true);

AddStep("enter setup mode", () => InputManager.Key(Key.F4));
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual);
AddAssert("switch track to real track", () =>
{
var setup = Editor.ChildrenOfType<SetupScreen>().First();

string temp = TestResources.GetTestBeatmapForImport();

string extractedFolder = $"{temp}_extracted";
Directory.CreateDirectory(extractedFolder);

try
{
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder);

bool success = setup.ChildrenOfType<ResourcesSection>().First().ChangeAudioTrack(new FileInfo(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3")));

// ensure audio file is copied to beatmap as "audio.mp3" rather than original filename.
Assert.That(Beatmap.Value.Metadata.AudioFile == "audio.mp3");

return success;
}
finally
{
File.Delete(temp);
Directory.Delete(extractedFolder, true);
}
});
AddAssert("switch track to real track", () => setAudio(applyToAllDifficulties: true, expected: "audio.mp3"));

AddAssert("track is not virtual", () => Beatmap.Value.Track is not TrackVirtual);
AddUntilStep("track length changed", () => Beatmap.Value.Track.Length > 60000);
Expand Down Expand Up @@ -530,5 +504,183 @@ public void TestCreateNewDifficultyForInconvertibleRuleset()
return set != null && set.PerformRead(s => s.Beatmaps.Count == 3 && s.Files.Count == 3);
});
}

[Test]
public void TestSingleBackgroundFile()
{
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
AddAssert("set background", () => setBackground(applyToAllDifficulties: true, expected: "bg.jpg"));

createNewDifficulty();
createNewDifficulty();

switchToDifficulty(1);

AddAssert("set background on second diff only", () => setBackground(applyToAllDifficulties: false, expected: "bg (1).jpg"));
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg"));

switchToDifficulty(0);

AddAssert("set background on first diff only", () => setBackground(applyToAllDifficulties: false, expected: "bg (2).jpg"));
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (2).jpg"));

AddAssert("set background on all diff", () => setBackground(applyToAllDifficulties: true, expected: "bg.jpg"));
AddAssert("all diff uses one background", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Metadata.BackgroundFile == "bg.jpg"));
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg.jpg"));
AddAssert("other files removed", () => !Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg" || f.Filename == "bg (2).jpg"));
}

[Test]
public void TestSingleAudioFile()
{
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
AddAssert("set audio", () => setAudio(applyToAllDifficulties: true, expected: "audio.mp3"));

createNewDifficulty();
createNewDifficulty();

switchToDifficulty(1);

AddAssert("set audio on second diff only", () => setAudio(applyToAllDifficulties: false, expected: "audio (1).mp3"));
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3"));

switchToDifficulty(0);

AddAssert("set audio on first diff only", () => setAudio(applyToAllDifficulties: false, expected: "audio (2).mp3"));
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (2).mp3"));

AddAssert("set audio on all diff", () => setAudio(applyToAllDifficulties: true, expected: "audio.mp3"));
AddAssert("all diff uses one audio", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Metadata.AudioFile == "audio.mp3"));
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio.mp3"));
AddAssert("other files removed", () => !Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3" || f.Filename == "audio (2).mp3"));
}

[Test]
public void TestMultipleBackgroundFiles()
{
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
AddAssert("set background", () => setBackground(applyToAllDifficulties: false, expected: "bg.jpg"));

createNewDifficulty();

AddAssert("new difficulty uses same background", () => Beatmap.Value.Metadata.BackgroundFile == "bg.jpg");
AddAssert("set background", () => setBackground(applyToAllDifficulties: false, expected: "bg (1).jpg"));
AddAssert("new difficulty uses new background", () => Beatmap.Value.Metadata.BackgroundFile == "bg (1).jpg");

switchToDifficulty(0);

AddAssert("old difficulty uses old background", () => Beatmap.Value.Metadata.BackgroundFile == "bg.jpg");
AddAssert("old background not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg.jpg"));
AddStep("set background", () => setBackground(applyToAllDifficulties: false, expected: "bg.jpg"));
AddAssert("other background not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg"));
}

[Test]
public void TestMultipleAudioFiles()
{
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
AddAssert("set audio", () => setAudio(applyToAllDifficulties: false, expected: "audio.mp3"));

createNewDifficulty();

AddAssert("new difficulty uses same audio", () => Beatmap.Value.Metadata.AudioFile == "audio.mp3");
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
AddUntilStep("wait for load", () => Editor.ChildrenOfType<SetupScreen>().Any());
AddAssert("set audio", () => setAudio(applyToAllDifficulties: false, expected: "audio (1).mp3"));
AddAssert("new difficulty uses new audio", () => Beatmap.Value.Metadata.AudioFile == "audio (1).mp3");

switchToDifficulty(0);

AddAssert("old difficulty uses old audio", () => Beatmap.Value.Metadata.AudioFile == "audio.mp3");
AddAssert("old audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio.mp3"));
AddStep("set audio", () => setAudio(applyToAllDifficulties: false, expected: "audio.mp3"));
AddAssert("other audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3"));
}

private void createNewDifficulty()
{
string? currentDifficulty = null;

AddStep("save", () => Editor.Save());
AddStep("create new difficulty", () =>
{
currentDifficulty = EditorBeatmap.BeatmapInfo.DifficultyName;
Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo);
});

AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction());
AddUntilStep("wait for created", () =>
{
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != currentDifficulty;
});

AddUntilStep("wait for editor load", () => Editor.IsLoaded);
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
AddUntilStep("wait for load", () => Editor.ChildrenOfType<SetupScreen>().Any());
}

private void switchToDifficulty(int index)
{
AddStep("save", () => Editor.Save());
AddStep($"switch to difficulty #{index + 1}", () =>
Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.ElementAt(index)));

AddUntilStep("wait for editor load", () => Editor.IsLoaded);
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
AddUntilStep("wait for load", () => Editor.ChildrenOfType<SetupScreen>().Any());
}

private bool setBackground(bool applyToAllDifficulties, string expected)
{
var setup = Editor.ChildrenOfType<SetupScreen>().First();

return setFile(TestResources.GetQuickTestBeatmapForImport(), extractedFolder =>
{
bool success = setup.ChildrenOfType<ResourcesSection>().First().ChangeBackgroundImage(
new FileInfo(Path.Combine(extractedFolder, @"machinetop_background.jpg")),
applyToAllDifficulties);

Assert.That(Beatmap.Value.Metadata.BackgroundFile, Is.EqualTo(expected));
return success;
});
}

private bool setAudio(bool applyToAllDifficulties, string expected)
{
var setup = Editor.ChildrenOfType<SetupScreen>().First();

return setFile(TestResources.GetTestBeatmapForImport(), extractedFolder =>
{
bool success = setup.ChildrenOfType<ResourcesSection>().First().ChangeAudioTrack(
new FileInfo(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3")),
applyToAllDifficulties);

Assert.That(Beatmap.Value.Metadata.AudioFile, Is.EqualTo(expected));
return success;
});
}

private bool setFile(string archivePath, Func<string, bool> func)
{
string temp = archivePath;

string extractedFolder = $"{temp}_extracted";
Directory.CreateDirectory(extractedFolder);

try
{
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder);

return func(extractedFolder);
}
finally
{
File.Delete(temp);
Directory.Delete(extractedFolder, true);
}
}
}
}
10 changes: 8 additions & 2 deletions osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Screens.Edit.Setup;
using osuTK;

namespace osu.Game.Tests.Visual.UserInterface
Expand Down Expand Up @@ -89,8 +90,13 @@ public TestSceneFormControls()
},
new FormFileSelector
{
Caption = "Audio file",
PlaceholderText = "Select an audio file",
Caption = "File selector",
PlaceholderText = "Select a file",
},
new FormBeatmapFileSelector(true)
{
Caption = "File selector with intermediate choice dialog",
PlaceholderText = "Select a file",
},
new FormColourPalette
{
Expand Down
30 changes: 25 additions & 5 deletions osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,20 +242,26 @@ Task ICanAcceptFiles.Import(params string[] paths)

Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException();

protected virtual FileChooserPopover CreatePopover(string[] handledExtensions, Bindable<FileInfo?> current, string? chooserPath) => new FileChooserPopover(handledExtensions, current, chooserPath);

public Popover GetPopover()
{
var popover = new FileChooserPopover(handledExtensions, Current, initialChooserPath);
var popover = CreatePopover(handledExtensions, Current, initialChooserPath);
popoverState.UnbindBindings();
popoverState.BindTo(popover.State);
return popover;
}

private partial class FileChooserPopover : OsuPopover
protected partial class FileChooserPopover : OsuPopover
{
protected override string PopInSampleName => "UI/overlay-big-pop-in";
protected override string PopOutSampleName => "UI/overlay-big-pop-out";

public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> currentFile, string? chooserPath)
private readonly Bindable<FileInfo?> current = new Bindable<FileInfo?>();

protected OsuFileSelector FileSelector;

public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> current, string? chooserPath)
: base(false)
{
Child = new Container
Expand All @@ -264,12 +270,13 @@ public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> 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 = new OsuFileSelector(chooserPath, handledExtensions)
Child = FileSelector = new OsuFileSelector(chooserPath, handledExtensions)
{
RelativeSizeAxes = Axes.Both,
CurrentFile = { BindTarget = currentFile }
},
};

this.current.BindTo(current);
}

[BackgroundDependencyLoader]
Expand All @@ -292,6 +299,19 @@ private void load(OverlayColourProvider colourProvider)
}
});
}

protected override void LoadComplete()
{
base.LoadComplete();

FileSelector.CurrentFile.ValueChanged += f =>
{
if (f.NewValue != null)
OnFileSelected(f.NewValue);
};
}

protected virtual void OnFileSelected(FileInfo file) => current.Value = file;
}
}
}
5 changes: 5 additions & 0 deletions osu.Game/Localisation/EditorSetupStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ public static class EditorSetupStrings
/// </summary>
public static LocalisableString AudioTrack => new TranslatableString(getKey(@"audio_track"), @"Audio Track");

/// <summary>
/// "Update all difficulties"
/// </summary>
public static LocalisableString ResourcesUpdateAllDifficulties => new TranslatableString(getKey(@"resources_update_all_difficulties"), @"Update all difficulties");

/// <summary>
/// "Click to select a track"
/// </summary>
Expand Down
Loading
Loading