mirror of
https://github.com/ppy/osu.git
synced 2025-01-13 02:32:55 +08:00
Merge pull request #28800 from peppy/file-mounting-v3
Add ability to edit beatmap content externally
This commit is contained in:
commit
f1fa34e5d5
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
@ -13,6 +14,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Rulesets.Mania;
|
using osu.Game.Rulesets.Mania;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
@ -29,30 +31,80 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
{
|
{
|
||||||
public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene
|
public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene
|
||||||
{
|
{
|
||||||
|
private BeatmapSetInfo beatmapSet = null!;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestExternalEditingNoChange()
|
||||||
|
{
|
||||||
|
string difficultyName = null!;
|
||||||
|
|
||||||
|
prepareBeatmap();
|
||||||
|
openEditor();
|
||||||
|
|
||||||
|
AddStep("store difficulty name", () => difficultyName = getEditor().Beatmap.Value.BeatmapInfo.DifficultyName);
|
||||||
|
|
||||||
|
AddStep("open file menu", () => getEditor().ChildrenOfType<Menu.DrawableMenuItem>().Single(m => m.Item.Text.Value.ToString() == "File").TriggerClick());
|
||||||
|
AddStep("click external edit", () => getEditor().ChildrenOfType<Menu.DrawableMenuItem>().Single(m => m.Item.Text.Value.ToString() == "Edit externally").TriggerClick());
|
||||||
|
|
||||||
|
AddUntilStep("wait for external edit screen", () => Game.ScreenStack.CurrentScreen is ExternalEditScreen externalEditScreen && externalEditScreen.IsLoaded);
|
||||||
|
|
||||||
|
AddUntilStep("wait for button ready", () => ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType<DangerousRoundedButton>().FirstOrDefault()?.Enabled.Value == true);
|
||||||
|
|
||||||
|
AddStep("finish external edit", () => ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType<DangerousRoundedButton>().First().TriggerClick());
|
||||||
|
|
||||||
|
AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||||
|
|
||||||
|
AddAssert("beatmapset didn't change", () => getEditor().Beatmap.Value.BeatmapSetInfo, () => Is.EqualTo(beatmapSet));
|
||||||
|
AddAssert("difficulty didn't change", () => getEditor().Beatmap.Value.BeatmapInfo.DifficultyName, () => Is.EqualTo(difficultyName));
|
||||||
|
AddAssert("old beatmapset not deleted", () => Game.BeatmapManager.QueryBeatmapSet(s => s.ID == beatmapSet.ID), () => Is.Not.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestExternalEditingWithChange()
|
||||||
|
{
|
||||||
|
string difficultyName = null!;
|
||||||
|
|
||||||
|
prepareBeatmap();
|
||||||
|
openEditor();
|
||||||
|
|
||||||
|
AddStep("store difficulty name", () => difficultyName = getEditor().Beatmap.Value.BeatmapInfo.DifficultyName);
|
||||||
|
|
||||||
|
AddStep("open file menu", () => getEditor().ChildrenOfType<Menu.DrawableMenuItem>().Single(m => m.Item.Text.Value.ToString() == "File").TriggerClick());
|
||||||
|
AddStep("click external edit", () => getEditor().ChildrenOfType<Menu.DrawableMenuItem>().Single(m => m.Item.Text.Value.ToString() == "Edit externally").TriggerClick());
|
||||||
|
|
||||||
|
AddUntilStep("wait for external edit screen", () => Game.ScreenStack.CurrentScreen is ExternalEditScreen externalEditScreen && externalEditScreen.IsLoaded);
|
||||||
|
|
||||||
|
AddUntilStep("wait for button ready", () => ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType<DangerousRoundedButton>().FirstOrDefault()?.Enabled.Value == true);
|
||||||
|
|
||||||
|
AddStep("add file externally", () =>
|
||||||
|
{
|
||||||
|
var op = ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).EditOperation!;
|
||||||
|
File.WriteAllText(Path.Combine(op.MountedPath, "test.txt"), "test");
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("finish external edit", () => ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType<DangerousRoundedButton>().First().TriggerClick());
|
||||||
|
|
||||||
|
AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||||
|
|
||||||
|
AddAssert("beatmapset changed", () => getEditor().Beatmap.Value.BeatmapSetInfo, () => Is.Not.EqualTo(beatmapSet));
|
||||||
|
AddAssert("beatmapset is locally modified", () => getEditor().Beatmap.Value.BeatmapSetInfo.Status, () => Is.EqualTo(BeatmapOnlineStatus.LocallyModified));
|
||||||
|
AddAssert("all difficulties are locally modified", () => getEditor().Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Status == BeatmapOnlineStatus.LocallyModified));
|
||||||
|
AddAssert("difficulty didn't change", () => getEditor().Beatmap.Value.BeatmapInfo.DifficultyName, () => Is.EqualTo(difficultyName));
|
||||||
|
AddAssert("old beatmapset deleted", () => Game.BeatmapManager.QueryBeatmapSet(s => s.ID == beatmapSet.ID), () => Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSaveThenDeleteActuallyDeletesAtSongSelect()
|
public void TestSaveThenDeleteActuallyDeletesAtSongSelect()
|
||||||
{
|
{
|
||||||
BeatmapSetInfo beatmapSet = null!;
|
prepareBeatmap();
|
||||||
|
openEditor();
|
||||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
|
||||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
|
||||||
|
|
||||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
|
||||||
AddUntilStep("wait for song select",
|
|
||||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
|
||||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
|
||||||
&& songSelect.IsLoaded);
|
|
||||||
|
|
||||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
|
||||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
|
||||||
|
|
||||||
makeMetadataChange();
|
makeMetadataChange();
|
||||||
|
|
||||||
AddAssert("save", () => Game.ChildrenOfType<Editor>().Single().Save());
|
AddAssert("save", () => getEditor().Save());
|
||||||
|
|
||||||
AddStep("delete beatmap", () => Game.BeatmapManager.Delete(beatmapSet));
|
AddStep("delete beatmap", () => Game.BeatmapManager.Delete(beatmapSet));
|
||||||
|
|
||||||
AddStep("exit", () => Game.ChildrenOfType<Editor>().Single().Exit());
|
AddStep("exit", () => getEditor().Exit());
|
||||||
|
|
||||||
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||||
&& songSelect.Beatmap.Value is DummyWorkingBeatmap);
|
&& songSelect.Beatmap.Value is DummyWorkingBeatmap);
|
||||||
@ -61,24 +113,14 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestChangeMetadataExitWhileTextboxFocusedPromptsSave()
|
public void TestChangeMetadataExitWhileTextboxFocusedPromptsSave()
|
||||||
{
|
{
|
||||||
BeatmapSetInfo beatmapSet = null!;
|
|
||||||
|
|
||||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
|
||||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
|
||||||
|
|
||||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
|
||||||
AddUntilStep("wait for song select",
|
|
||||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
|
||||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
|
||||||
&& songSelect.IsLoaded);
|
|
||||||
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||||
|
|
||||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
prepareBeatmap();
|
||||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
openEditor();
|
||||||
|
|
||||||
makeMetadataChange(commit: false);
|
makeMetadataChange(commit: false);
|
||||||
|
|
||||||
AddStep("exit", () => Game.ChildrenOfType<Editor>().Single().Exit());
|
AddStep("exit", () => getEditor().Exit());
|
||||||
|
|
||||||
AddUntilStep("save dialog displayed", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault()?.CurrentDialog is PromptForSaveDialog);
|
AddUntilStep("save dialog displayed", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault()?.CurrentDialog is PromptForSaveDialog);
|
||||||
}
|
}
|
||||||
@ -121,16 +163,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
|
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
|
||||||
{
|
{
|
||||||
BeatmapSetInfo beatmapSet = null!;
|
prepareBeatmap();
|
||||||
|
|
||||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
|
||||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
|
||||||
|
|
||||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
|
||||||
AddUntilStep("wait for song select",
|
|
||||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
|
||||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
|
||||||
&& songSelect.IsLoaded);
|
|
||||||
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||||
|
|
||||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||||
@ -183,19 +217,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestExitEditorWithoutSelection()
|
public void TestExitEditorWithoutSelection()
|
||||||
{
|
{
|
||||||
BeatmapSetInfo beatmapSet = null!;
|
prepareBeatmap();
|
||||||
|
openEditor();
|
||||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
|
||||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
|
||||||
|
|
||||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
|
||||||
AddUntilStep("wait for song select",
|
|
||||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
|
||||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
|
||||||
&& songSelect.IsLoaded);
|
|
||||||
|
|
||||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
|
||||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
|
||||||
|
|
||||||
AddStep("escape once", () => InputManager.Key(Key.Escape));
|
AddStep("escape once", () => InputManager.Key(Key.Escape));
|
||||||
|
|
||||||
@ -205,19 +228,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestExitEditorWithSelection()
|
public void TestExitEditorWithSelection()
|
||||||
{
|
{
|
||||||
BeatmapSetInfo beatmapSet = null!;
|
prepareBeatmap();
|
||||||
|
openEditor();
|
||||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
|
||||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
|
||||||
|
|
||||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
|
||||||
AddUntilStep("wait for song select",
|
|
||||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
|
||||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
|
||||||
&& songSelect.IsLoaded);
|
|
||||||
|
|
||||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
|
||||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
|
||||||
|
|
||||||
AddStep("make selection", () =>
|
AddStep("make selection", () =>
|
||||||
{
|
{
|
||||||
@ -239,19 +251,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestLastTimestampRememberedOnExit()
|
public void TestLastTimestampRememberedOnExit()
|
||||||
{
|
{
|
||||||
BeatmapSetInfo beatmapSet = null!;
|
prepareBeatmap();
|
||||||
|
openEditor();
|
||||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
|
||||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
|
||||||
|
|
||||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
|
||||||
AddUntilStep("wait for song select",
|
|
||||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
|
||||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
|
||||||
&& songSelect.IsLoaded);
|
|
||||||
|
|
||||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
|
||||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
|
||||||
|
|
||||||
AddStep("seek to arbitrary time", () => getEditor().ChildrenOfType<EditorClock>().First().Seek(1234));
|
AddStep("seek to arbitrary time", () => getEditor().ChildrenOfType<EditorClock>().First().Seek(1234));
|
||||||
AddUntilStep("time is correct", () => getEditor().ChildrenOfType<EditorClock>().First().CurrentTime, () => Is.EqualTo(1234));
|
AddUntilStep("time is correct", () => getEditor().ChildrenOfType<EditorClock>().First().CurrentTime, () => Is.EqualTo(1234));
|
||||||
@ -259,32 +260,21 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
AddStep("exit editor", () => InputManager.Key(Key.Escape));
|
AddStep("exit editor", () => InputManager.Key(Key.Escape));
|
||||||
AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor);
|
AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor);
|
||||||
|
|
||||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit());
|
openEditor();
|
||||||
|
|
||||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
|
||||||
AddUntilStep("time is correct", () => getEditor().ChildrenOfType<EditorClock>().First().CurrentTime, () => Is.EqualTo(1234));
|
AddUntilStep("time is correct", () => getEditor().ChildrenOfType<EditorClock>().First().CurrentTime, () => Is.EqualTo(1234));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestAttemptGlobalMusicOperationFromEditor()
|
public void TestAttemptGlobalMusicOperationFromEditor()
|
||||||
{
|
{
|
||||||
BeatmapSetInfo beatmapSet = null!;
|
prepareBeatmap();
|
||||||
|
|
||||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
|
||||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
|
||||||
|
|
||||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
|
||||||
AddUntilStep("wait for song select",
|
|
||||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
|
||||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
|
||||||
&& songSelect.IsLoaded);
|
|
||||||
|
|
||||||
AddUntilStep("wait for music playing", () => Game.MusicController.IsPlaying);
|
AddUntilStep("wait for music playing", () => Game.MusicController.IsPlaying);
|
||||||
AddStep("user request stop", () => Game.MusicController.Stop(requestedByUser: true));
|
AddStep("user request stop", () => Game.MusicController.Stop(requestedByUser: true));
|
||||||
AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying);
|
AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying);
|
||||||
|
|
||||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
openEditor();
|
||||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
|
||||||
|
|
||||||
AddUntilStep("music still stopped", () => !Game.MusicController.IsPlaying);
|
AddUntilStep("music still stopped", () => !Game.MusicController.IsPlaying);
|
||||||
AddStep("user request play", () => Game.MusicController.Play(requestedByUser: true));
|
AddStep("user request play", () => Game.MusicController.Play(requestedByUser: true));
|
||||||
@ -302,20 +292,10 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
[TestCase(SortMode.Difficulty)]
|
[TestCase(SortMode.Difficulty)]
|
||||||
public void TestSelectionRetainedOnExit(SortMode sortMode)
|
public void TestSelectionRetainedOnExit(SortMode sortMode)
|
||||||
{
|
{
|
||||||
BeatmapSetInfo beatmapSet = null!;
|
|
||||||
|
|
||||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
|
||||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
|
||||||
|
|
||||||
AddStep($"set sort mode to {sortMode}", () => Game.LocalConfig.SetValue(OsuSetting.SongSelectSortingMode, sortMode));
|
AddStep($"set sort mode to {sortMode}", () => Game.LocalConfig.SetValue(OsuSetting.SongSelectSortingMode, sortMode));
|
||||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
|
||||||
AddUntilStep("wait for song select",
|
|
||||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
|
||||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
|
||||||
&& songSelect.IsLoaded);
|
|
||||||
|
|
||||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
prepareBeatmap();
|
||||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
openEditor();
|
||||||
|
|
||||||
AddStep("exit editor", () => InputManager.Key(Key.Escape));
|
AddStep("exit editor", () => InputManager.Key(Key.Escape));
|
||||||
AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor);
|
AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor);
|
||||||
@ -332,6 +312,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
|
|
||||||
AddStep("open editor", () => Game.ChildrenOfType<ButtonSystem>().Single().OnEditBeatmap?.Invoke());
|
AddStep("open editor", () => Game.ChildrenOfType<ButtonSystem>().Single().OnEditBeatmap?.Invoke());
|
||||||
AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded);
|
AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded);
|
||||||
|
|
||||||
AddStep("click on file", () =>
|
AddStep("click on file", () =>
|
||||||
{
|
{
|
||||||
var item = getEditor().ChildrenOfType<Menu.DrawableMenuItem>().Single(i => i.Item.Text.Value.ToString() == "File");
|
var item = getEditor().ChildrenOfType<Menu.DrawableMenuItem>().Single(i => i.Item.Text.Value.ToString() == "File");
|
||||||
@ -354,6 +335,24 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
AddAssert("editor beatmap uses catch ruleset", () => getEditorBeatmap().BeatmapInfo.Ruleset.ShortName == "fruits");
|
AddAssert("editor beatmap uses catch ruleset", () => getEditorBeatmap().BeatmapInfo.Ruleset.ShortName == "fruits");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void prepareBeatmap()
|
||||||
|
{
|
||||||
|
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||||
|
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||||
|
|
||||||
|
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||||
|
AddUntilStep("wait for song select",
|
||||||
|
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||||
|
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||||
|
&& songSelect.IsLoaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openEditor()
|
||||||
|
{
|
||||||
|
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||||
|
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||||
|
}
|
||||||
|
|
||||||
private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType<EditorBeatmap>().Single();
|
private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType<EditorBeatmap>().Single();
|
||||||
|
|
||||||
private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;
|
private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;
|
||||||
|
@ -415,6 +415,9 @@ namespace osu.Game.Beatmaps
|
|||||||
public Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original) =>
|
public Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original) =>
|
||||||
beatmapImporter.ImportAsUpdate(notification, importTask, original);
|
beatmapImporter.ImportAsUpdate(notification, importTask, original);
|
||||||
|
|
||||||
|
public Task<ExternalEditOperation<BeatmapSetInfo>> BeginExternalEditing(BeatmapSetInfo model) =>
|
||||||
|
beatmapImporter.BeginExternalEditing(model);
|
||||||
|
|
||||||
public Task Export(BeatmapSetInfo beatmap) => beatmapExporter.ExportAsync(beatmap.ToLive(Realm));
|
public Task Export(BeatmapSetInfo beatmap) => beatmapExporter.ExportAsync(beatmap.ToLive(Realm));
|
||||||
|
|
||||||
public Task ExportLegacy(BeatmapSetInfo beatmap) => legacyBeatmapExporter.ExportAsync(beatmap.ToLive(Realm));
|
public Task ExportLegacy(BeatmapSetInfo beatmap) => legacyBeatmapExporter.ExportAsync(beatmap.ToLive(Realm));
|
||||||
|
65
osu.Game/Database/ExternalEditOperation.cs
Normal file
65
osu.Game/Database/ExternalEditOperation.cs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// 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.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
|
|
||||||
|
namespace osu.Game.Database
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains information related to an active external edit operation.
|
||||||
|
/// </summary>
|
||||||
|
public class ExternalEditOperation<TModel> where TModel : class, IHasGuidPrimaryKey
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The temporary path at which the model has been exported to for editing.
|
||||||
|
/// </summary>
|
||||||
|
public readonly string MountedPath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the model is still mounted at <see cref="MountedPath"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsMounted { get; private set; }
|
||||||
|
|
||||||
|
private readonly IModelImporter<TModel> importer;
|
||||||
|
private readonly TModel original;
|
||||||
|
|
||||||
|
public ExternalEditOperation(IModelImporter<TModel> importer, TModel original, string path)
|
||||||
|
{
|
||||||
|
this.importer = importer;
|
||||||
|
this.original = original;
|
||||||
|
|
||||||
|
MountedPath = path;
|
||||||
|
|
||||||
|
IsMounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finish the external edit operation.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This will trigger an asynchronous reimport of the model.
|
||||||
|
/// Subsequent calls will be a no-op.
|
||||||
|
/// </remarks>
|
||||||
|
/// <returns>A task which will eventuate in the newly imported model with changes applied.</returns>
|
||||||
|
public async Task<Live<TModel>?> Finish()
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(MountedPath) || !IsMounted)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
IsMounted = false;
|
||||||
|
|
||||||
|
Live<TModel>? imported = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(MountedPath), original)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(MountedPath, true);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
return imported;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -34,6 +34,15 @@ namespace osu.Game.Database
|
|||||||
/// <returns>The imported model.</returns>
|
/// <returns>The imported model.</returns>
|
||||||
Task<Live<TModel>?> ImportAsUpdate(ProgressNotification notification, ImportTask task, TModel original);
|
Task<Live<TModel>?> ImportAsUpdate(ProgressNotification notification, ImportTask task, TModel original);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mount all files for a model to a temporary directory to allow for external editing.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// When editing is completed, call Finish() on the returned operation class to begin the import-and-update process.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="model">The model to mount.</param>
|
||||||
|
public Task<ExternalEditOperation<TModel>> BeginExternalEditing(TModel model);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A user displayable name for the model type associated with this manager.
|
/// A user displayable name for the model type associated with this manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -179,6 +179,30 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
public virtual Task<Live<TModel>?> ImportAsUpdate(ProgressNotification notification, ImportTask task, TModel original) => throw new NotImplementedException();
|
public virtual Task<Live<TModel>?> ImportAsUpdate(ProgressNotification notification, ImportTask task, TModel original) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public async Task<ExternalEditOperation<TModel>> BeginExternalEditing(TModel model)
|
||||||
|
{
|
||||||
|
string mountedPath = Path.Join(Path.GetTempPath(), model.Hash);
|
||||||
|
|
||||||
|
if (Directory.Exists(mountedPath))
|
||||||
|
Directory.Delete(mountedPath, true);
|
||||||
|
|
||||||
|
Directory.CreateDirectory(mountedPath);
|
||||||
|
|
||||||
|
foreach (var realmFile in model.Files)
|
||||||
|
{
|
||||||
|
string sourcePath = Files.Storage.GetFullPath(realmFile.File.GetStoragePath());
|
||||||
|
string destinationPath = Path.Join(mountedPath, realmFile.Filename);
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
|
||||||
|
|
||||||
|
using (var inStream = Files.Storage.GetStream(sourcePath))
|
||||||
|
using (var outStream = File.Create(destinationPath))
|
||||||
|
await inStream.CopyToAsync(outStream).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ExternalEditOperation<TModel>(this, model, mountedPath);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Import one <typeparamref name="TModel"/> from the filesystem and delete the file on success.
|
/// Import one <typeparamref name="TModel"/> from the filesystem and delete the file on success.
|
||||||
/// Note that this bypasses the UI flow and should only be used for special cases or testing.
|
/// Note that this bypasses the UI flow and should only be used for special cases or testing.
|
||||||
|
@ -15,10 +15,10 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.IO.Archives;
|
using osu.Game.IO.Archives;
|
||||||
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Overlays.Notifications;
|
using osu.Game.Overlays.Notifications;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Online.API;
|
|
||||||
using osu.Game.Scoring.Legacy;
|
using osu.Game.Scoring.Legacy;
|
||||||
|
|
||||||
namespace osu.Game.Scoring
|
namespace osu.Game.Scoring
|
||||||
@ -214,6 +214,7 @@ namespace osu.Game.Scoring
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Task<Live<ScoreInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask task, ScoreInfo original) => scoreImporter.ImportAsUpdate(notification, task, original);
|
public Task<Live<ScoreInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask task, ScoreInfo original) => scoreImporter.ImportAsUpdate(notification, task, original);
|
||||||
|
public Task<ExternalEditOperation<ScoreInfo>> BeginExternalEditing(ScoreInfo model) => scoreImporter.BeginExternalEditing(model);
|
||||||
|
|
||||||
public Live<ScoreInfo>? Import(ScoreInfo item, ArchiveReader? archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>
|
public Live<ScoreInfo>? Import(ScoreInfo item, ArchiveReader? archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>
|
||||||
scoreImporter.ImportModel(item, archive, parameters, cancellationToken);
|
scoreImporter.ImportModel(item, archive, parameters, cancellationToken);
|
||||||
|
@ -481,7 +481,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
{
|
{
|
||||||
if (HasUnsavedChanges)
|
if (HasUnsavedChanges)
|
||||||
{
|
{
|
||||||
dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to test it.", () => attemptMutationOperation(() =>
|
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() =>
|
||||||
{
|
{
|
||||||
if (!Save()) return false;
|
if (!Save()) return false;
|
||||||
|
|
||||||
@ -1112,6 +1112,10 @@ namespace osu.Game.Screens.Edit
|
|||||||
var export = createExportMenu();
|
var export = createExportMenu();
|
||||||
saveRelatedMenuItems.AddRange(export.Items);
|
saveRelatedMenuItems.AddRange(export.Items);
|
||||||
yield return export;
|
yield return export;
|
||||||
|
|
||||||
|
var externalEdit = new EditorMenuItem("Edit externally", MenuItemType.Standard, editExternally);
|
||||||
|
saveRelatedMenuItems.Add(externalEdit);
|
||||||
|
yield return externalEdit;
|
||||||
}
|
}
|
||||||
|
|
||||||
yield return new OsuMenuItemSpacer();
|
yield return new OsuMenuItemSpacer();
|
||||||
@ -1129,11 +1133,35 @@ namespace osu.Game.Screens.Edit
|
|||||||
return new EditorMenuItem(CommonStrings.Export) { Items = exportItems };
|
return new EditorMenuItem(CommonStrings.Export) { Items = exportItems };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void editExternally()
|
||||||
|
{
|
||||||
|
if (HasUnsavedChanges)
|
||||||
|
{
|
||||||
|
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() =>
|
||||||
|
{
|
||||||
|
if (!Save())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
startEdit();
|
||||||
|
return true;
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
startEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void startEdit()
|
||||||
|
{
|
||||||
|
this.Push(new ExternalEditScreen());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void exportBeatmap(bool legacy)
|
private void exportBeatmap(bool legacy)
|
||||||
{
|
{
|
||||||
if (HasUnsavedChanges)
|
if (HasUnsavedChanges)
|
||||||
{
|
{
|
||||||
dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to export it.", () => attemptAsyncMutationOperation(() =>
|
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptAsyncMutationOperation(() =>
|
||||||
{
|
{
|
||||||
if (!Save())
|
if (!Save())
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@ -1211,17 +1239,14 @@ namespace osu.Game.Screens.Edit
|
|||||||
{
|
{
|
||||||
if (isNewBeatmap)
|
if (isNewBeatmap)
|
||||||
{
|
{
|
||||||
dialogOverlay.Push(new SaveRequiredPopupDialog("This beatmap will be saved in order to create another difficulty.", () =>
|
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() =>
|
||||||
{
|
|
||||||
attemptMutationOperation(() =>
|
|
||||||
{
|
{
|
||||||
if (!Save())
|
if (!Save())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
CreateNewDifficulty(rulesetInfo);
|
CreateNewDifficulty(rulesetInfo);
|
||||||
return true;
|
return true;
|
||||||
});
|
})));
|
||||||
}));
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1260,7 +1285,11 @@ namespace osu.Game.Screens.Edit
|
|||||||
return new EditorMenuItem(EditorStrings.ChangeDifficulty) { Items = difficultyItems };
|
return new EditorMenuItem(EditorStrings.ChangeDifficulty) { Items = difficultyItems };
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset));
|
public void SwitchToDifficulty(BeatmapInfo nextBeatmap)
|
||||||
|
{
|
||||||
|
switchingDifficulty = true;
|
||||||
|
loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset));
|
||||||
|
}
|
||||||
|
|
||||||
private void cancelExit()
|
private void cancelExit()
|
||||||
{
|
{
|
||||||
|
252
osu.Game/Screens/Edit/ExternalEditScreen.cs
Normal file
252
osu.Game/Screens/Edit/ExternalEditScreen.cs
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
// 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.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Screens;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Edit
|
||||||
|
{
|
||||||
|
internal partial class ExternalEditScreen : OsuScreen
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private GameHost gameHost { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapManager beatmapManager { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private Editor editor { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||||
|
|
||||||
|
private Task? fileMountOperation;
|
||||||
|
|
||||||
|
public ExternalEditOperation<BeatmapSetInfo>? EditOperation;
|
||||||
|
|
||||||
|
private FillFlowContainer flow = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
InternalChild = new Container
|
||||||
|
{
|
||||||
|
Masking = true,
|
||||||
|
CornerRadius = 20,
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
AutoSizeDuration = 500,
|
||||||
|
AutoSizeEasing = Easing.OutQuint,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Colour = colourProvider.Background5,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
flow = new FillFlowContainer
|
||||||
|
{
|
||||||
|
Margin = new MarginPadding(20),
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Spacing = new Vector2(15),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
fileMountOperation = begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool OnExiting(ScreenExitEvent e)
|
||||||
|
{
|
||||||
|
// Don't allow exiting until the file mount operation has completed.
|
||||||
|
// This is mainly to simplify the flow (once the screen is pushed we are guaranteed an attempted mount).
|
||||||
|
if (fileMountOperation?.IsCompleted == false)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// If the operation completed successfully, ensure that we finish the operation before exiting.
|
||||||
|
// The finish() call will subsequently call Exit() when done.
|
||||||
|
if (EditOperation != null)
|
||||||
|
{
|
||||||
|
finish().FireAndForget();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnExiting(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task begin()
|
||||||
|
{
|
||||||
|
showSpinner("Exporting for edit...");
|
||||||
|
|
||||||
|
await Task.Delay(500).ConfigureAwait(true);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EditOperation = await beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!).ConfigureAwait(true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Log($@"Failed to initiate external edit operation: {ex}", LoggingTarget.Database);
|
||||||
|
fileMountOperation = null;
|
||||||
|
showSpinner("Export failed!");
|
||||||
|
await Task.Delay(1000).ConfigureAwait(true);
|
||||||
|
this.Exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
flow.Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Text = "Beatmap is mounted externally",
|
||||||
|
Font = OsuFont.Default.With(size: 30),
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
},
|
||||||
|
new OsuTextFlowContainer
|
||||||
|
{
|
||||||
|
Padding = new MarginPadding(5),
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Width = 350,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Text = "Any changes made to the exported folder will be imported to the game, including file additions, modifications and deletions.",
|
||||||
|
},
|
||||||
|
new PurpleRoundedButton
|
||||||
|
{
|
||||||
|
Text = "Open folder",
|
||||||
|
Width = 350,
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Action = openDirectory,
|
||||||
|
Enabled = { Value = false }
|
||||||
|
},
|
||||||
|
new DangerousRoundedButton
|
||||||
|
{
|
||||||
|
Text = "Finish editing and import changes",
|
||||||
|
Width = 350,
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Action = () => finish().FireAndForget(),
|
||||||
|
Enabled = { Value = false }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Scheduler.AddDelayed(() =>
|
||||||
|
{
|
||||||
|
foreach (var b in flow.ChildrenOfType<RoundedButton>())
|
||||||
|
b.Enabled.Value = true;
|
||||||
|
openDirectory();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openDirectory()
|
||||||
|
{
|
||||||
|
if (EditOperation == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Ensure the trailing separator is present in order to show the folder contents.
|
||||||
|
gameHost.OpenFileExternally(EditOperation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task finish()
|
||||||
|
{
|
||||||
|
string originalDifficulty = editor.Beatmap.Value.Beatmap.BeatmapInfo.DifficultyName;
|
||||||
|
|
||||||
|
showSpinner("Cleaning up...");
|
||||||
|
|
||||||
|
Live<BeatmapSetInfo>? beatmap = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
beatmap = await EditOperation!.Finish().ConfigureAwait(true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Log($@"Failed to finish external edit operation: {ex}", LoggingTarget.Database);
|
||||||
|
showSpinner("Import failed!");
|
||||||
|
await Task.Delay(1000).ConfigureAwait(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setting to null will allow exit to succeed.
|
||||||
|
EditOperation = null;
|
||||||
|
|
||||||
|
if (beatmap == null)
|
||||||
|
this.Exit();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// the `ImportAsUpdate()` flow will yield beatmap(sets) with online status of `None` if online lookup fails.
|
||||||
|
// coerce such models to `LocallyModified` state instead to unify behaviour with normal editing flow.
|
||||||
|
beatmap.PerformWrite(s =>
|
||||||
|
{
|
||||||
|
if (s.Status == BeatmapOnlineStatus.None)
|
||||||
|
s.Status = BeatmapOnlineStatus.LocallyModified;
|
||||||
|
foreach (var difficulty in s.Beatmaps.Where(b => b.Status == BeatmapOnlineStatus.None))
|
||||||
|
difficulty.Status = BeatmapOnlineStatus.LocallyModified;
|
||||||
|
});
|
||||||
|
|
||||||
|
var closestMatchingBeatmap =
|
||||||
|
beatmap.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalDifficulty)
|
||||||
|
?? beatmap.Value.Beatmaps.First();
|
||||||
|
|
||||||
|
editor.SwitchToDifficulty(closestMatchingBeatmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSpinner(string text)
|
||||||
|
{
|
||||||
|
foreach (var b in flow.ChildrenOfType<RoundedButton>())
|
||||||
|
b.Enabled.Value = false;
|
||||||
|
|
||||||
|
flow.Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
Font = OsuFont.Default.With(size: 30),
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
},
|
||||||
|
new LoadingSpinner
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
State = { Value = Visibility.Visible }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,9 +9,9 @@ namespace osu.Game.Screens.Edit
|
|||||||
{
|
{
|
||||||
public partial class SaveRequiredPopupDialog : PopupDialog
|
public partial class SaveRequiredPopupDialog : PopupDialog
|
||||||
{
|
{
|
||||||
public SaveRequiredPopupDialog(string headerText, Action saveAndAction)
|
public SaveRequiredPopupDialog(Action saveAndAction)
|
||||||
{
|
{
|
||||||
HeaderText = headerText;
|
HeaderText = "The beatmap will be saved to continue with this operation.";
|
||||||
|
|
||||||
Icon = FontAwesome.Regular.Save;
|
Icon = FontAwesome.Regular.Save;
|
||||||
|
|
||||||
|
@ -312,6 +312,8 @@ namespace osu.Game.Skinning
|
|||||||
public Task<Live<SkinInfo>> ImportAsUpdate(ProgressNotification notification, ImportTask task, SkinInfo original) =>
|
public Task<Live<SkinInfo>> ImportAsUpdate(ProgressNotification notification, ImportTask task, SkinInfo original) =>
|
||||||
skinImporter.ImportAsUpdate(notification, task, original);
|
skinImporter.ImportAsUpdate(notification, task, original);
|
||||||
|
|
||||||
|
public Task<ExternalEditOperation<SkinInfo>> BeginExternalEditing(SkinInfo model) => skinImporter.BeginExternalEditing(model);
|
||||||
|
|
||||||
public Task<Live<SkinInfo>> Import(ImportTask task, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>
|
public Task<Live<SkinInfo>> Import(ImportTask task, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>
|
||||||
skinImporter.Import(task, parameters, cancellationToken);
|
skinImporter.Import(task, parameters, cancellationToken);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user