1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-28 06:42:54 +08:00

Merge pull request #19413 from Gramore/feature/EditorDeleteDiff

Add ability to delete difficulties from the editor
This commit is contained in:
Salman Ahmed 2022-09-06 19:20:45 +03:00 committed by GitHub
commit 13efa819ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 181 additions and 25 deletions

View File

@ -0,0 +1,85 @@
// 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.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Storyboards;
using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneDifficultyDelete : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override bool IsolateSavingFromDatabase => false;
[Resolved]
private OsuGameBase game { get; set; } = null!;
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
private BeatmapSetInfo importedBeatmapSet = null!;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null!)
=> beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First());
public override void SetUpSteps()
{
AddStep("import test beatmap", () => importedBeatmapSet = BeatmapImportHelper.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely());
base.SetUpSteps();
}
[Test]
public void TestDeleteDifficulties()
{
Guid deletedDifficultyID = Guid.Empty;
int countBeforeDeletion = 0;
string beatmapSetHashBefore = string.Empty;
for (int i = 0; i < 12; i++)
{
// Will be reloaded after each deletion.
AddUntilStep("wait for editor to load", () => Editor?.ReadyForUse == true);
AddStep("store selected difficulty", () =>
{
deletedDifficultyID = EditorBeatmap.BeatmapInfo.ID;
countBeforeDeletion = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count;
beatmapSetHashBefore = Beatmap.Value.BeatmapSetInfo.Hash;
});
AddStep("click File", () => this.ChildrenOfType<DrawableOsuMenuItem>().First().TriggerClick());
if (i == 11)
{
// last difficulty shouldn't be able to be deleted.
AddAssert("Delete menu item disabled", () => getDeleteMenuItem().Item.Action.Disabled);
}
else
{
AddStep("click delete", () => getDeleteMenuItem().TriggerClick());
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null);
AddStep("confirm", () => InputManager.Key(Key.Number1));
AddAssert($"difficulty {i} is deleted", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Select(b => b.ID), () => Does.Not.Contain(deletedDifficultyID));
AddAssert("count decreased by one", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, () => Is.EqualTo(countBeforeDeletion - 1));
AddAssert("set hash changed", () => Beatmap.Value.BeatmapSetInfo.Hash, () => Is.Not.EqualTo(beatmapSetHashBefore));
}
}
}
private DrawableOsuMenuItem getDeleteMenuItem() => this.ChildrenOfType<DrawableOsuMenuItem>()
.Single(item => item.ChildrenOfType<SpriteText>().Any(text => text.Text.ToString().StartsWith("Delete", StringComparison.Ordinal)));
}
}

View File

@ -319,8 +319,7 @@ namespace osu.Game.Beatmaps
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
setInfo.Hash = beatmapImporter.ComputeHash(setInfo);
setInfo.Status = BeatmapOnlineStatus.LocallyModified;
updateHashAndMarkDirty(setInfo);
Realm.Write(r =>
{
@ -363,6 +362,33 @@ namespace osu.Game.Beatmaps
});
}
/// <summary>
/// Delete a beatmap difficulty immediately.
/// </summary>
/// <remarks>
/// There's no undoing this operation, as we don't have a soft-deletion flag on <see cref="BeatmapInfo"/>.
/// This may be a future consideration if there's a user requirement for undeleting support.
/// </remarks>
public void DeleteDifficultyImmediately(BeatmapInfo beatmapInfo)
{
Realm.Write(r =>
{
if (!beatmapInfo.IsManaged)
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID);
Debug.Assert(beatmapInfo.BeatmapSet != null);
Debug.Assert(beatmapInfo.File != null);
var setInfo = beatmapInfo.BeatmapSet;
DeleteFile(setInfo, beatmapInfo.File);
setInfo.Beatmaps.Remove(beatmapInfo);
updateHashAndMarkDirty(setInfo);
workingBeatmapCache.Invalidate(setInfo);
});
}
/// <summary>
/// Delete videos from a list of beatmaps.
/// This will post notifications tracking progress.
@ -416,6 +442,12 @@ namespace osu.Game.Beatmaps
public Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original) =>
beatmapImporter.ImportAsUpdate(notification, importTask, original);
private void updateHashAndMarkDirty(BeatmapSetInfo setInfo)
{
setInfo.Hash = beatmapImporter.ComputeHash(setInfo);
setInfo.Status = BeatmapOnlineStatus.LocallyModified;
}
#region Implementation of ICanAcceptFiles
public Task Import(params string[] paths) => beatmapImporter.Import(paths);

View File

@ -0,0 +1,18 @@
// 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.
using System;
using osu.Game.Beatmaps;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Screens.Edit
{
public class DeleteDifficultyConfirmationDialog : DeleteConfirmationDialog
{
public DeleteDifficultyConfirmationDialog(BeatmapInfo beatmapInfo, Action deleteAction)
{
BodyText = $"\"{beatmapInfo.DifficultyName}\" difficulty";
DeleteAction = deleteAction;
}
}
}

View File

@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework;
@ -879,35 +878,61 @@ namespace osu.Game.Screens.Edit
clock.SeekForward(!trackPlaying, amount);
}
private void updateLastSavedHash()
{
lastSavedHash = changeHandler?.CurrentStateHash;
}
private List<MenuItem> createFileMenuItems() => new List<MenuItem>
{
new EditorMenuItem("Save", MenuItemType.Standard, () => Save()),
new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
new EditorMenuItemSpacer(),
createDifficultyCreationMenu(),
createDifficultySwitchMenu(),
new EditorMenuItemSpacer(),
new EditorMenuItem("Delete difficulty", MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } },
new EditorMenuItemSpacer(),
new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit)
};
private void exportBeatmap()
{
Save();
new LegacyBeatmapExporter(storage).Export(Beatmap.Value.BeatmapSetInfo);
}
private void updateLastSavedHash()
{
lastSavedHash = changeHandler?.CurrentStateHash;
}
/// <summary>
/// Beatmaps of the currently edited set, grouped by ruleset and ordered by difficulty.
/// </summary>
private IOrderedEnumerable<IGrouping<RulesetInfo, BeatmapInfo>> groupedOrderedBeatmaps => Beatmap.Value.BeatmapSetInfo.Beatmaps
.OrderBy(b => b.StarRating)
.GroupBy(b => b.Ruleset)
.OrderBy(group => group.Key);
private List<MenuItem> createFileMenuItems()
private void deleteDifficulty()
{
var fileMenuItems = new List<MenuItem>
if (dialogOverlay == null)
delete();
else
dialogOverlay.Push(new DeleteDifficultyConfirmationDialog(Beatmap.Value.BeatmapInfo, delete));
void delete()
{
new EditorMenuItem("Save", MenuItemType.Standard, () => Save())
};
BeatmapInfo difficultyToDelete = playableBeatmap.BeatmapInfo;
if (RuntimeInfo.IsDesktop)
fileMenuItems.Add(new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap));
var difficultiesBeforeDeletion = groupedOrderedBeatmaps.SelectMany(g => g).ToList();
fileMenuItems.Add(new EditorMenuItemSpacer());
beatmapManager.DeleteDifficultyImmediately(difficultyToDelete);
fileMenuItems.Add(createDifficultyCreationMenu());
fileMenuItems.Add(createDifficultySwitchMenu());
int deletedIndex = difficultiesBeforeDeletion.IndexOf(difficultyToDelete);
// of note, we're still working with the cloned version, so indices are all prior to deletion.
BeatmapInfo nextToShow = difficultiesBeforeDeletion[deletedIndex == 0 ? 1 : deletedIndex - 1];
fileMenuItems.Add(new EditorMenuItemSpacer());
fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit));
return fileMenuItems;
Beatmap.Value = beatmapManager.GetWorkingBeatmap(nextToShow);
SwitchToDifficulty(nextToShow);
}
}
private EditorMenuItem createDifficultyCreationMenu()
@ -939,18 +964,14 @@ namespace osu.Game.Screens.Edit
private EditorMenuItem createDifficultySwitchMenu()
{
var beatmapSet = playableBeatmap.BeatmapInfo.BeatmapSet;
Debug.Assert(beatmapSet != null);
var difficultyItems = new List<MenuItem>();
foreach (var rulesetBeatmaps in beatmapSet.Beatmaps.GroupBy(b => b.Ruleset).OrderBy(group => group.Key))
foreach (var rulesetBeatmaps in groupedOrderedBeatmaps)
{
if (difficultyItems.Count > 0)
difficultyItems.Add(new EditorMenuItemSpacer());
foreach (var beatmap in rulesetBeatmaps.OrderBy(b => b.StarRating))
foreach (var beatmap in rulesetBeatmaps)
{
bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmap);
difficultyItems.Add(new DifficultyMenuItem(beatmap, isCurrentDifficulty, SwitchToDifficulty));