diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index d1b8e88743..777d5db2ad 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -20,7 +20,6 @@ using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
-using osu.Game.Screens.Edit;
using osu.Game.Skinning;
using osu.Game.Stores;
@@ -109,55 +108,73 @@ namespace osu.Game.Beatmaps
}
///
- /// Add a new difficulty to the beatmap set represented by the provided .
+ /// Add a new difficulty to the provided based on the provided .
/// The new difficulty will be backed by a model
/// and represented by the returned .
///
- public virtual WorkingBeatmap CreateNewDifficulty(NewDifficultyCreationParameters creationParameters)
+ ///
+ /// Contrary to , this method does not preserve hitobjects and beatmap-level settings from .
+ /// The created beatmap will have zero hitobjects and will have default settings (including difficulty settings), but will preserve metadata and existing timing points.
+ ///
+ /// The to add the new difficulty to.
+ /// The to use as a baseline reference when creating the new difficulty.
+ /// The ruleset with which the new difficulty should be created.
+ public virtual WorkingBeatmap CreateNewDifficulty(BeatmapSetInfo targetBeatmapSet, WorkingBeatmap referenceWorkingBeatmap, RulesetInfo rulesetInfo)
{
- var referenceBeatmap = creationParameters.ReferenceBeatmap;
- var targetBeatmapSet = creationParameters.BeatmapSet;
+ var playableBeatmap = referenceWorkingBeatmap.GetPlayableBeatmap(rulesetInfo);
+ var newBeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), playableBeatmap.Metadata.DeepClone());
+ var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo };
+ foreach (var timingPoint in playableBeatmap.ControlPointInfo.TimingPoints)
+ newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone());
+
+ return addDifficultyToSet(targetBeatmapSet, newBeatmap, referenceWorkingBeatmap.Skin);
+ }
+
+ ///
+ /// Add a copy of the provided to the provided .
+ /// The new difficulty will be backed by a model
+ /// and represented by the returned .
+ ///
+ ///
+ /// Contrary to , this method creates a nearly-exact copy of
+ /// (with the exception of a few key properties that cannot be copied under any circumstance, like difficulty name, beatmap hash, or online status).
+ ///
+ /// The to add the copy to.
+ /// The to be copied.
+ public virtual WorkingBeatmap CopyExistingDifficulty(BeatmapSetInfo targetBeatmapSet, WorkingBeatmap referenceWorkingBeatmap)
+ {
+ var newBeatmap = referenceWorkingBeatmap.GetPlayableBeatmap(referenceWorkingBeatmap.BeatmapInfo.Ruleset).Clone();
BeatmapInfo newBeatmapInfo;
- IBeatmap newBeatmap;
- if (creationParameters.CreateBlank)
- {
- newBeatmapInfo = new BeatmapInfo(creationParameters.Ruleset, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone());
- newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo };
- foreach (var timingPoint in referenceBeatmap.ControlPointInfo.TimingPoints)
- newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone());
- }
- else
- {
- newBeatmap = referenceBeatmap.Clone();
- newBeatmap.BeatmapInfo = newBeatmapInfo = referenceBeatmap.BeatmapInfo.Clone();
- // assign a new ID to the clone.
- newBeatmapInfo.ID = Guid.NewGuid();
- // add "(copy)" suffix to difficulty name to avoid clashes on save.
- newBeatmapInfo.DifficultyName += " (copy)";
- // clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps.
- newBeatmapInfo.Hash = string.Empty;
- // clear online properties.
- newBeatmapInfo.OnlineID = -1;
- newBeatmapInfo.Status = BeatmapOnlineStatus.None;
- }
+ newBeatmap.BeatmapInfo = newBeatmapInfo = referenceWorkingBeatmap.BeatmapInfo.Clone();
+ // assign a new ID to the clone.
+ newBeatmapInfo.ID = Guid.NewGuid();
+ // add "(copy)" suffix to difficulty name to avoid clashes on save.
+ newBeatmapInfo.DifficultyName += " (copy)";
+ // clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps.
+ newBeatmapInfo.Hash = string.Empty;
+ // clear online properties.
+ newBeatmapInfo.OnlineID = -1;
+ newBeatmapInfo.Status = BeatmapOnlineStatus.None;
+ return addDifficultyToSet(targetBeatmapSet, newBeatmap, referenceWorkingBeatmap.Skin);
+ }
+
+ private WorkingBeatmap addDifficultyToSet(BeatmapSetInfo targetBeatmapSet, IBeatmap newBeatmap, ISkin beatmapSkin)
+ {
// populate circular beatmap set info <-> beatmap info references manually.
// several places like `BeatmapModelManager.Save()` or `GetWorkingBeatmap()`
// rely on them being freely traversable in both directions for correct operation.
- targetBeatmapSet.Beatmaps.Add(newBeatmapInfo);
- newBeatmapInfo.BeatmapSet = targetBeatmapSet;
+ targetBeatmapSet.Beatmaps.Add(newBeatmap.BeatmapInfo);
+ newBeatmap.BeatmapInfo.BeatmapSet = targetBeatmapSet;
- beatmapModelManager.Save(newBeatmapInfo, newBeatmap, creationParameters.ReferenceBeatmapSkin);
+ beatmapModelManager.Save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin);
workingBeatmapCache.Invalidate(targetBeatmapSet);
return GetWorkingBeatmap(newBeatmap.BeatmapInfo);
}
- // TODO: add back support for making a copy of another difficulty
- // (likely via a separate `CopyDifficulty()` method).
-
///
/// Delete a beatmap difficulty.
///
diff --git a/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs b/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs
index 138e13bda1..aa6ca280ee 100644
--- a/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs
+++ b/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs
@@ -10,11 +10,11 @@ namespace osu.Game.Screens.Edit
{
///
/// Delegate used to create new difficulties.
- /// A value of in the clearAllObjects parameter
- /// indicates that the new difficulty should have its hitobjects cleared;
- /// otherwise, the new difficulty should be an exact copy of an existing one.
+ /// A value of in the createCopy parameter
+ /// indicates that the new difficulty should be an exact copy of an existing one;
+ /// otherwise, the new difficulty should have its hitobjects and beatmap-level settings cleared.
///
- public delegate void CreateNewDifficulty(bool clearAllObjects);
+ public delegate void CreateNewDifficulty(bool createCopy);
public CreateNewDifficultyDialog(CreateNewDifficulty createNewDifficulty)
{
@@ -27,12 +27,12 @@ namespace osu.Game.Screens.Edit
new PopupDialogOkButton
{
Text = "Yeah, let's start from scratch!",
- Action = () => createNewDifficulty.Invoke(true)
+ Action = () => createNewDifficulty.Invoke(false)
},
new PopupDialogCancelButton
{
Text = "No, create an exact copy of this difficulty",
- Action = () => createNewDifficulty.Invoke(false)
+ Action = () => createNewDifficulty.Invoke(true)
},
new PopupDialogCancelButton
{
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 7a3c4f2a19..c2775ae101 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -9,7 +9,6 @@ using JetBrains.Annotations;
using osu.Framework;
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;
@@ -359,14 +358,14 @@ namespace osu.Game.Screens.Edit
///
/// Creates an instance representing the current state of the editor.
///
- ///
- /// The next beatmap to be shown, in the case of difficulty switch.
+ ///
+ /// The ruleset of the next beatmap to be shown, in the case of difficulty switch.
/// indicates that the beatmap will not be changing.
///
- public EditorState GetState([CanBeNull] BeatmapInfo nextBeatmap = null) => new EditorState
+ public EditorState GetState([CanBeNull] RulesetInfo nextRuleset = null) => new EditorState
{
Time = clock.CurrentTimeAccurate,
- ClipboardContent = nextBeatmap == null || editorBeatmap.BeatmapInfo.Ruleset.ShortName == nextBeatmap.Ruleset.ShortName ? Clipboard.Content.Value : string.Empty
+ ClipboardContent = nextRuleset == null || editorBeatmap.BeatmapInfo.Ruleset.ShortName == nextRuleset.ShortName ? Clipboard.Content.Value : string.Empty
};
///
@@ -845,23 +844,15 @@ namespace osu.Game.Screens.Edit
{
if (!rulesetInfo.Equals(editorBeatmap.BeatmapInfo.Ruleset))
{
- switchToNewDifficulty(rulesetInfo, true);
+ switchToNewDifficulty(rulesetInfo, false);
return;
}
- dialogOverlay.Push(new CreateNewDifficultyDialog(clearAllObjects => switchToNewDifficulty(rulesetInfo, clearAllObjects)));
+ dialogOverlay.Push(new CreateNewDifficultyDialog(createCopy => switchToNewDifficulty(rulesetInfo, createCopy)));
}
- private void switchToNewDifficulty(RulesetInfo rulesetInfo, bool clearAllObjects)
- => loader?.ScheduleSwitchToNewDifficulty(new NewDifficultyCreationParameters
- (
- editorBeatmap.BeatmapInfo.BeatmapSet.AsNonNull(),
- rulesetInfo,
- editorBeatmap,
- editorBeatmap.BeatmapSkin,
- clearAllObjects,
- GetState()
- ));
+ private void switchToNewDifficulty(RulesetInfo rulesetInfo, bool createCopy)
+ => loader?.ScheduleSwitchToNewDifficulty(editorBeatmap.BeatmapInfo, rulesetInfo, createCopy, GetState(rulesetInfo));
private EditorMenuItem createDifficultySwitchMenu()
{
@@ -886,7 +877,7 @@ namespace osu.Game.Screens.Edit
return new EditorMenuItem("Change difficulty") { Items = difficultyItems };
}
- protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap));
+ protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset));
private void cancelExit()
{
diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs
index 505a57f157..0a2b8437fa 100644
--- a/osu.Game/Screens/Edit/EditorLoader.cs
+++ b/osu.Game/Screens/Edit/EditorLoader.cs
@@ -4,6 +4,7 @@
using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
@@ -11,6 +12,7 @@ using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
@@ -79,19 +81,18 @@ namespace osu.Game.Screens.Edit
}
}
- public void ScheduleSwitchToNewDifficulty(NewDifficultyCreationParameters creationParameters)
+ public void ScheduleSwitchToNewDifficulty(BeatmapInfo referenceBeatmapInfo, RulesetInfo rulesetInfo, bool createCopy, EditorState editorState)
=> scheduleDifficultySwitch(() =>
{
try
{
- var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(creationParameters.ReferenceBeatmap.BeatmapInfo);
- return beatmapManager.CreateNewDifficulty(new NewDifficultyCreationParameters(
- refetchedBeatmap.BeatmapSetInfo,
- refetchedBeatmap.BeatmapInfo.Ruleset,
- refetchedBeatmap.Beatmap,
- refetchedBeatmap.Skin,
- creationParameters.CreateBlank,
- creationParameters.EditorState));
+ // fetch a fresh detached reference from database to avoid polluting model instances attached to cached working beatmaps.
+ var targetBeatmapSet = beatmapManager.QueryBeatmap(b => b.ID == referenceBeatmapInfo.ID).AsNonNull().BeatmapSet.AsNonNull();
+ var referenceWorkingBeatmap = beatmapManager.GetWorkingBeatmap(referenceBeatmapInfo);
+
+ return createCopy
+ ? beatmapManager.CopyExistingDifficulty(targetBeatmapSet, referenceWorkingBeatmap)
+ : beatmapManager.CreateNewDifficulty(targetBeatmapSet, referenceWorkingBeatmap, rulesetInfo);
}
catch (Exception ex)
{
@@ -100,7 +101,7 @@ namespace osu.Game.Screens.Edit
Logger.Error(ex, ex.Message);
return Beatmap.Value;
}
- }, creationParameters.EditorState);
+ }, editorState);
public void ScheduleSwitchToExistingDifficulty(BeatmapInfo beatmapInfo, EditorState editorState)
=> scheduleDifficultySwitch(() => beatmapManager.GetWorkingBeatmap(beatmapInfo), editorState);
diff --git a/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs b/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs
deleted file mode 100644
index a6458a9456..0000000000
--- a/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable enable
-
-using osu.Game.Beatmaps;
-using osu.Game.Rulesets;
-using osu.Game.Skinning;
-
-namespace osu.Game.Screens.Edit
-{
- public class NewDifficultyCreationParameters
- {
- ///
- /// The that should contain the newly-created difficulty.
- ///
- public BeatmapSetInfo BeatmapSet { get; }
-
- ///
- /// The that the new difficulty should be playable for.
- ///
- public RulesetInfo Ruleset { get; }
-
- ///
- /// A reference upon which the new difficulty should be based.
- ///
- public IBeatmap ReferenceBeatmap { get; }
-
- ///
- /// A reference that the new difficulty should base its own skin upon.
- ///
- public ISkin? ReferenceBeatmapSkin { get; }
-
- ///
- /// Whether the new difficulty should be blank.
- ///
- ///
- /// A blank difficulty will have no objects, no control points other than timing points taken from
- /// and will not share values with ,
- /// but it will share metadata and timing information with .
- ///
- public bool CreateBlank { get; }
-
- ///
- /// The saved state of the previous which should be restored upon opening the newly-created difficulty.
- ///
- public EditorState EditorState { get; }
-
- public NewDifficultyCreationParameters(
- BeatmapSetInfo beatmapSet,
- RulesetInfo ruleset,
- IBeatmap referenceBeatmap,
- ISkin? referenceBeatmapSkin,
- bool createBlank,
- EditorState editorState)
- {
- BeatmapSet = beatmapSet;
- Ruleset = ruleset;
- ReferenceBeatmap = referenceBeatmap;
- ReferenceBeatmapSkin = referenceBeatmapSkin;
- CreateBlank = createBlank;
- EditorState = editorState;
- }
- }
-}
diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs
index 8c8a106791..24015590e2 100644
--- a/osu.Game/Tests/Visual/EditorTestScene.cs
+++ b/osu.Game/Tests/Visual/EditorTestScene.cs
@@ -136,7 +136,13 @@ namespace osu.Game.Tests.Visual
return new TestWorkingBeatmapCache(this, audioManager, resources, storage, defaultBeatmap, host);
}
- public override WorkingBeatmap CreateNewDifficulty(NewDifficultyCreationParameters creationParameters)
+ public override WorkingBeatmap CreateNewDifficulty(BeatmapSetInfo targetBeatmapSet, WorkingBeatmap referenceWorkingBeatmap, RulesetInfo rulesetInfo)
+ {
+ // don't actually care about properly creating a difficulty for this context.
+ return TestBeatmap;
+ }
+
+ public override WorkingBeatmap CopyExistingDifficulty(BeatmapSetInfo targetBeatmapSet, WorkingBeatmap referenceWorkingBeatmap)
{
// don't actually care about properly creating a difficulty for this context.
return TestBeatmap;