mirror of
https://github.com/ppy/osu.git
synced 2025-02-15 17:33:02 +08:00
Merge pull request #14640 from bdach/editor-new-change-diff
Add support for changing current beatmap set difficulty from within editor
This commit is contained in:
commit
5026485d4d
170
osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
Normal file
170
osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
// 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.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Overlays.Dialog;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
using osu.Game.Screens.Edit.Components.Menus;
|
||||||
|
using osu.Game.Screens.Menu;
|
||||||
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
|
{
|
||||||
|
public class TestSceneDifficultySwitching : ScreenTestScene
|
||||||
|
{
|
||||||
|
private BeatmapSetInfo importedBeatmapSet;
|
||||||
|
private Editor editor;
|
||||||
|
|
||||||
|
// required for screen transitions to work properly
|
||||||
|
// (see comment in EditorLoader.LogoArriving).
|
||||||
|
[Cached]
|
||||||
|
private OsuLogo logo = new OsuLogo
|
||||||
|
{
|
||||||
|
Alpha = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuGameBase game { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapManager beatmaps { get; set; }
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load() => Add(logo);
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
AddStep("import test beatmap", () => importedBeatmapSet = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result);
|
||||||
|
|
||||||
|
AddStep("set current beatmap", () => Beatmap.Value = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First()));
|
||||||
|
AddStep("push loader", () => Stack.Push(new EditorLoader()));
|
||||||
|
|
||||||
|
AddUntilStep("wait for editor push", () => Stack.CurrentScreen is Editor);
|
||||||
|
AddStep("store editor", () => editor = (Editor)Stack.CurrentScreen);
|
||||||
|
AddUntilStep("wait for editor to load", () => editor.IsLoaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBasicSwitch()
|
||||||
|
{
|
||||||
|
BeatmapInfo targetDifficulty = null;
|
||||||
|
|
||||||
|
AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
|
||||||
|
switchToDifficulty(() => targetDifficulty);
|
||||||
|
confirmEditingBeatmap(() => targetDifficulty);
|
||||||
|
|
||||||
|
AddStep("exit editor", () => Stack.Exit());
|
||||||
|
// ensure editor loader didn't resume.
|
||||||
|
AddAssert("stack empty", () => Stack.CurrentScreen == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPreventSwitchDueToUnsavedChanges()
|
||||||
|
{
|
||||||
|
BeatmapInfo targetDifficulty = null;
|
||||||
|
PromptForSaveDialog saveDialog = null;
|
||||||
|
|
||||||
|
AddStep("remove first hitobject", () =>
|
||||||
|
{
|
||||||
|
var editorBeatmap = editor.ChildrenOfType<EditorBeatmap>().Single();
|
||||||
|
editorBeatmap.RemoveAt(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
|
||||||
|
switchToDifficulty(() => targetDifficulty);
|
||||||
|
|
||||||
|
AddUntilStep("prompt for save dialog shown", () =>
|
||||||
|
{
|
||||||
|
saveDialog = this.ChildrenOfType<PromptForSaveDialog>().Single();
|
||||||
|
return saveDialog != null;
|
||||||
|
});
|
||||||
|
AddStep("continue editing", () =>
|
||||||
|
{
|
||||||
|
var continueButton = saveDialog.ChildrenOfType<PopupDialogCancelButton>().Last();
|
||||||
|
continueButton.TriggerClick();
|
||||||
|
});
|
||||||
|
|
||||||
|
confirmEditingBeatmap(() => importedBeatmapSet.Beatmaps.First());
|
||||||
|
|
||||||
|
AddRepeatStep("exit editor forcefully", () => Stack.Exit(), 2);
|
||||||
|
// ensure editor loader didn't resume.
|
||||||
|
AddAssert("stack empty", () => Stack.CurrentScreen == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAllowSwitchAfterDiscardingUnsavedChanges()
|
||||||
|
{
|
||||||
|
BeatmapInfo targetDifficulty = null;
|
||||||
|
PromptForSaveDialog saveDialog = null;
|
||||||
|
|
||||||
|
AddStep("remove first hitobject", () =>
|
||||||
|
{
|
||||||
|
var editorBeatmap = editor.ChildrenOfType<EditorBeatmap>().Single();
|
||||||
|
editorBeatmap.RemoveAt(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
|
||||||
|
switchToDifficulty(() => targetDifficulty);
|
||||||
|
|
||||||
|
AddUntilStep("prompt for save dialog shown", () =>
|
||||||
|
{
|
||||||
|
saveDialog = this.ChildrenOfType<PromptForSaveDialog>().Single();
|
||||||
|
return saveDialog != null;
|
||||||
|
});
|
||||||
|
AddStep("discard changes", () =>
|
||||||
|
{
|
||||||
|
var continueButton = saveDialog.ChildrenOfType<PopupDialogOkButton>().Single();
|
||||||
|
continueButton.TriggerClick();
|
||||||
|
});
|
||||||
|
|
||||||
|
confirmEditingBeatmap(() => targetDifficulty);
|
||||||
|
|
||||||
|
AddStep("exit editor forcefully", () => Stack.Exit());
|
||||||
|
// ensure editor loader didn't resume.
|
||||||
|
AddAssert("stack empty", () => Stack.CurrentScreen == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void switchToDifficulty(Func<BeatmapInfo> difficulty)
|
||||||
|
{
|
||||||
|
AddUntilStep("wait for menubar to load", () => editor.ChildrenOfType<EditorMenuBar>().Any());
|
||||||
|
AddStep("open file menu", () =>
|
||||||
|
{
|
||||||
|
var menuBar = editor.ChildrenOfType<EditorMenuBar>().Single();
|
||||||
|
var fileMenu = menuBar.ChildrenOfType<DrawableOsuMenuItem>().First();
|
||||||
|
InputManager.MoveMouseTo(fileMenu);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("open difficulty menu", () =>
|
||||||
|
{
|
||||||
|
var difficultySelector =
|
||||||
|
editor.ChildrenOfType<DrawableOsuMenuItem>().Single(item => item.Item.Text.Value.ToString().Contains("Change difficulty"));
|
||||||
|
InputManager.MoveMouseTo(difficultySelector);
|
||||||
|
});
|
||||||
|
AddWaitStep("wait for open", 3);
|
||||||
|
|
||||||
|
AddStep("switch to target difficulty", () =>
|
||||||
|
{
|
||||||
|
var difficultyMenuItem =
|
||||||
|
editor.ChildrenOfType<DrawableOsuMenuItem>()
|
||||||
|
.Last(item => item.Item is DifficultyMenuItem difficultyItem && difficultyItem.Beatmap.Equals(difficulty.Invoke()));
|
||||||
|
InputManager.MoveMouseTo(difficultyMenuItem);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void confirmEditingBeatmap(Func<BeatmapInfo> targetDifficulty)
|
||||||
|
{
|
||||||
|
AddUntilStep("current beatmap is correct", () => Beatmap.Value.BeatmapInfo.Equals(targetDifficulty.Invoke()));
|
||||||
|
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs
Normal file
27
osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// 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.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Edit.Components.Menus
|
||||||
|
{
|
||||||
|
public class DifficultyMenuItem : StatefulMenuItem<bool>
|
||||||
|
{
|
||||||
|
public BeatmapInfo Beatmap { get; }
|
||||||
|
|
||||||
|
public DifficultyMenuItem(BeatmapInfo beatmapInfo, bool selected, Action<BeatmapInfo> difficultyChangeFunc)
|
||||||
|
: base(beatmapInfo.Version ?? "(unnamed)", null)
|
||||||
|
{
|
||||||
|
Beatmap = beatmapInfo;
|
||||||
|
State.Value = selected;
|
||||||
|
|
||||||
|
if (!selected)
|
||||||
|
Action.Value = () => difficultyChangeFunc.Invoke(beatmapInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IconUsage? GetIconForState(bool state) => state ? (IconUsage?)FontAwesome.Solid.Check : null;
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework;
|
using osu.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -75,6 +76,9 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
private Container<EditorScreen> screenContainer;
|
private Container<EditorScreen> screenContainer;
|
||||||
|
|
||||||
|
[CanBeNull]
|
||||||
|
private readonly EditorLoader loader;
|
||||||
|
|
||||||
private EditorScreen currentScreen;
|
private EditorScreen currentScreen;
|
||||||
|
|
||||||
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
|
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
|
||||||
@ -101,6 +105,11 @@ namespace osu.Game.Screens.Edit
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private MusicController music { get; set; }
|
private MusicController music { get; set; }
|
||||||
|
|
||||||
|
public Editor(EditorLoader loader = null)
|
||||||
|
{
|
||||||
|
this.loader = loader;
|
||||||
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours, OsuConfigManager config)
|
private void load(OsuColour colours, OsuConfigManager config)
|
||||||
{
|
{
|
||||||
@ -489,7 +498,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
if (isNewBeatmap || HasUnsavedChanges)
|
if (isNewBeatmap || HasUnsavedChanges)
|
||||||
{
|
{
|
||||||
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave));
|
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave, cancelExit));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -703,11 +712,38 @@ namespace osu.Game.Screens.Edit
|
|||||||
if (RuntimeInfo.IsDesktop)
|
if (RuntimeInfo.IsDesktop)
|
||||||
fileMenuItems.Add(new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap));
|
fileMenuItems.Add(new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap));
|
||||||
|
|
||||||
|
fileMenuItems.Add(new EditorMenuItemSpacer());
|
||||||
|
|
||||||
|
var beatmapSet = beatmapManager.QueryBeatmapSet(bs => bs.ID == Beatmap.Value.BeatmapSetInfo.ID) ?? playableBeatmap.BeatmapInfo.BeatmapSet;
|
||||||
|
|
||||||
|
var difficultyItems = new List<MenuItem>();
|
||||||
|
|
||||||
|
foreach (var rulesetBeatmaps in beatmapSet.Beatmaps.GroupBy(b => b.RulesetID).OrderBy(group => group.Key))
|
||||||
|
{
|
||||||
|
if (difficultyItems.Count > 0)
|
||||||
|
difficultyItems.Add(new EditorMenuItemSpacer());
|
||||||
|
|
||||||
|
foreach (var beatmap in rulesetBeatmaps.OrderBy(b => b.StarDifficulty))
|
||||||
|
difficultyItems.Add(createDifficultyMenuItem(beatmap));
|
||||||
|
}
|
||||||
|
|
||||||
|
fileMenuItems.Add(new EditorMenuItem("Change difficulty") { Items = difficultyItems });
|
||||||
|
|
||||||
fileMenuItems.Add(new EditorMenuItemSpacer());
|
fileMenuItems.Add(new EditorMenuItemSpacer());
|
||||||
fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit));
|
fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit));
|
||||||
return fileMenuItems;
|
return fileMenuItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DifficultyMenuItem createDifficultyMenuItem(BeatmapInfo beatmapInfo)
|
||||||
|
{
|
||||||
|
bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmapInfo);
|
||||||
|
return new DifficultyMenuItem(beatmapInfo, isCurrentDifficulty, switchToDifficulty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void switchToDifficulty(BeatmapInfo beatmapInfo) => loader?.ScheduleDifficultySwitch(beatmapInfo);
|
||||||
|
|
||||||
|
private void cancelExit() => loader?.CancelPendingDifficultySwitch();
|
||||||
|
|
||||||
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
|
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
|
||||||
|
|
||||||
public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime);
|
public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime);
|
||||||
|
94
osu.Game/Screens/Edit/EditorLoader.cs
Normal file
94
osu.Game/Screens/Edit/EditorLoader.cs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
// 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 JetBrains.Annotations;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Screens;
|
||||||
|
using osu.Framework.Threading;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Screens.Menu;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Edit
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Transition screen for the editor.
|
||||||
|
/// Used to avoid backing out to main menu/song select when switching difficulties from within the editor.
|
||||||
|
/// </summary>
|
||||||
|
public class EditorLoader : ScreenWithBeatmapBackground
|
||||||
|
{
|
||||||
|
public override float BackgroundParallaxAmount => 0.1f;
|
||||||
|
|
||||||
|
public override bool AllowBackButton => false;
|
||||||
|
|
||||||
|
public override bool HideOverlaysOnEnter => true;
|
||||||
|
|
||||||
|
public override bool DisallowExternalBeatmapRulesetChanges => true;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapManager beatmapManager { get; set; }
|
||||||
|
|
||||||
|
[CanBeNull]
|
||||||
|
private ScheduledDelegate scheduledDifficultySwitch;
|
||||||
|
|
||||||
|
protected override void LogoArriving(OsuLogo logo, bool resuming)
|
||||||
|
{
|
||||||
|
base.LogoArriving(logo, resuming);
|
||||||
|
|
||||||
|
if (!resuming)
|
||||||
|
{
|
||||||
|
// the push cannot happen in OnEntering() or similar (even if scheduled), because the transition from main menu will look bad.
|
||||||
|
// that is because this screen pushing the editor makes it no longer current, and OsuScreen checks if the screen is current
|
||||||
|
// before enqueueing this screen's LogoArriving onto the logo animation sequence.
|
||||||
|
pushEditor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
AddRangeInternal(new Drawable[]
|
||||||
|
{
|
||||||
|
new LoadingSpinner(true)
|
||||||
|
{
|
||||||
|
State = { Value = Visibility.Visible },
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ScheduleDifficultySwitch(BeatmapInfo beatmapInfo)
|
||||||
|
{
|
||||||
|
scheduledDifficultySwitch?.Cancel();
|
||||||
|
ValidForResume = true;
|
||||||
|
|
||||||
|
this.MakeCurrent();
|
||||||
|
|
||||||
|
scheduledDifficultySwitch = Schedule(() =>
|
||||||
|
{
|
||||||
|
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo);
|
||||||
|
|
||||||
|
// This screen is a weird exception to the rule that nothing after song select changes the global beatmap.
|
||||||
|
// Because of this, we need to update the background stack's beatmap to match.
|
||||||
|
// If we don't do this, the editor will see a discrepancy and create a new background, along with an unnecessary transition.
|
||||||
|
ApplyToBackground(b => b.Beatmap = Beatmap.Value);
|
||||||
|
|
||||||
|
pushEditor();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pushEditor()
|
||||||
|
{
|
||||||
|
this.Push(new Editor(this));
|
||||||
|
ValidForResume = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CancelPendingDifficultySwitch()
|
||||||
|
{
|
||||||
|
scheduledDifficultySwitch?.Cancel();
|
||||||
|
ValidForResume = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
{
|
{
|
||||||
public class PromptForSaveDialog : PopupDialog
|
public class PromptForSaveDialog : PopupDialog
|
||||||
{
|
{
|
||||||
public PromptForSaveDialog(Action exit, Action saveAndExit)
|
public PromptForSaveDialog(Action exit, Action saveAndExit, Action cancel)
|
||||||
{
|
{
|
||||||
HeaderText = "Did you want to save your changes?";
|
HeaderText = "Did you want to save your changes?";
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
new PopupDialogCancelButton
|
new PopupDialogCancelButton
|
||||||
{
|
{
|
||||||
Text = @"Oops, continue editing",
|
Text = @"Oops, continue editing",
|
||||||
|
Action = cancel
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@ namespace osu.Game.Screens.Menu
|
|||||||
OnEdit = delegate
|
OnEdit = delegate
|
||||||
{
|
{
|
||||||
Beatmap.SetDefault();
|
Beatmap.SetDefault();
|
||||||
this.Push(new Editor());
|
this.Push(new EditorLoader());
|
||||||
},
|
},
|
||||||
OnSolo = loadSoloSongSelect,
|
OnSolo = loadSoloSongSelect,
|
||||||
OnMultiplayer = () => this.Push(new Multiplayer()),
|
OnMultiplayer = () => this.Push(new Multiplayer()),
|
||||||
|
@ -349,7 +349,7 @@ namespace osu.Game.Screens.Select
|
|||||||
throw new InvalidOperationException($"Attempted to edit when {nameof(AllowEditing)} is disabled");
|
throw new InvalidOperationException($"Attempted to edit when {nameof(AllowEditing)} is disabled");
|
||||||
|
|
||||||
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap ?? beatmapNoDebounce);
|
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap ?? beatmapNoDebounce);
|
||||||
this.Push(new Editor());
|
this.Push(new EditorLoader());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
Loading…
Reference in New Issue
Block a user