Restructure difficulty copy flow to adapt to latest changes

This commit is contained in:
Bartłomiej Dach 2022-02-14 20:56:05 +01:00
parent 6221447164
commit e45a2ae0fc
No known key found for this signature in database
GPG Key ID: BCECCD4FA41F6497
6 changed files with 83 additions and 133 deletions

View File

@ -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
}
/// <summary>
/// Add a new difficulty to the beatmap set represented by the provided <see cref="BeatmapSetInfo"/>.
/// Add a new difficulty to the provided <paramref name="targetBeatmapSet"/> based on the provided <paramref name="referenceWorkingBeatmap"/>.
/// The new difficulty will be backed by a <see cref="BeatmapInfo"/> model
/// and represented by the returned <see cref="WorkingBeatmap"/>.
/// </summary>
public virtual WorkingBeatmap CreateNewDifficulty(NewDifficultyCreationParameters creationParameters)
/// <remarks>
/// Contrary to <see cref="CopyExistingDifficulty"/>, this method does not preserve hitobjects and beatmap-level settings from <paramref name="referenceWorkingBeatmap"/>.
/// The created beatmap will have zero hitobjects and will have default settings (including difficulty settings), but will preserve metadata and existing timing points.
/// </remarks>
/// <param name="targetBeatmapSet">The <see cref="BeatmapSetInfo"/> to add the new difficulty to.</param>
/// <param name="referenceWorkingBeatmap">The <see cref="WorkingBeatmap"/> to use as a baseline reference when creating the new difficulty.</param>
/// <param name="rulesetInfo">The ruleset with which the new difficulty should be created.</param>
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);
}
/// <summary>
/// Add a copy of the provided <paramref name="referenceWorkingBeatmap"/> to the provided <paramref name="targetBeatmapSet"/>.
/// The new difficulty will be backed by a <see cref="BeatmapInfo"/> model
/// and represented by the returned <see cref="WorkingBeatmap"/>.
/// </summary>
/// <remarks>
/// Contrary to <see cref="CreateNewDifficulty"/>, this method creates a nearly-exact copy of <paramref name="referenceWorkingBeatmap"/>
/// (with the exception of a few key properties that cannot be copied under any circumstance, like difficulty name, beatmap hash, or online status).
/// </remarks>
/// <param name="targetBeatmapSet">The <see cref="BeatmapSetInfo"/> to add the copy to.</param>
/// <param name="referenceWorkingBeatmap">The <see cref="WorkingBeatmap"/> to be copied.</param>
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).
/// <summary>
/// Delete a beatmap difficulty.
/// </summary>

View File

@ -10,11 +10,11 @@ namespace osu.Game.Screens.Edit
{
/// <summary>
/// Delegate used to create new difficulties.
/// A value of <see langword="true"/> in the <c>clearAllObjects</c> 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 <see langword="true"/> in the <c>createCopy</c> 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.
/// </summary>
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
{

View File

@ -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
/// <summary>
/// Creates an <see cref="EditorState"/> instance representing the current state of the editor.
/// </summary>
/// <param name="nextBeatmap">
/// The next beatmap to be shown, in the case of difficulty switch.
/// <param name="nextRuleset">
/// The ruleset of the next beatmap to be shown, in the case of difficulty switch.
/// <see langword="null"/> indicates that the beatmap will not be changing.
/// </param>
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
};
/// <summary>
@ -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()
{

View File

@ -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);

View File

@ -1,65 +0,0 @@
// 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.
#nullable enable
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Skinning;
namespace osu.Game.Screens.Edit
{
public class NewDifficultyCreationParameters
{
/// <summary>
/// The <see cref="BeatmapSetInfo"/> that should contain the newly-created difficulty.
/// </summary>
public BeatmapSetInfo BeatmapSet { get; }
/// <summary>
/// The <see cref="RulesetInfo"/> that the new difficulty should be playable for.
/// </summary>
public RulesetInfo Ruleset { get; }
/// <summary>
/// A reference <see cref="IBeatmap"/> upon which the new difficulty should be based.
/// </summary>
public IBeatmap ReferenceBeatmap { get; }
/// <summary>
/// A reference <see cref="ISkin"/> that the new difficulty should base its own skin upon.
/// </summary>
public ISkin? ReferenceBeatmapSkin { get; }
/// <summary>
/// Whether the new difficulty should be blank.
/// </summary>
/// <remarks>
/// A blank difficulty will have no objects, no control points other than timing points taken from <see cref="ReferenceBeatmap"/>
/// and will not share <see cref="BeatmapInfo"/> values with <see cref="ReferenceBeatmap"/>,
/// but it will share metadata and timing information with <see cref="ReferenceBeatmap"/>.
/// </remarks>
public bool CreateBlank { get; }
/// <summary>
/// The saved state of the previous <see cref="Editor"/> which should be restored upon opening the newly-created difficulty.
/// </summary>
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;
}
}
}

View File

@ -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;