1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 11:27:24 +08:00

Merge branch 'master' into spectator-reset-speed-at-end

This commit is contained in:
Salman Ahmed 2022-06-21 06:37:58 +03:00 committed by GitHub
commit f103de4efb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 327 additions and 145 deletions

View File

@ -126,6 +126,12 @@ jobs:
with:
dotnet-version: "6.0.x"
# macOS agents recently have empty NuGet config files, resulting in restore failures,
# see https://github.com/actions/virtual-environments/issues/5768
# Add the global nuget package source manually for now.
- name: Setup NuGet.Config
run: echo '<configuration><packageSources><add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" /></packageSources></configuration>' > ~/.config/NuGet/NuGet.Config
# Contrary to seemingly any other msbuild, msbuild running on macOS/Mono
# cannot accept .sln(f) files as arguments.
# Build just the main game for now.

View File

@ -154,7 +154,7 @@ namespace osu.Desktop.Updater
Activated = () =>
{
updateManager.PrepareUpdateAsync()
.ContinueWith(_ => updateManager.Schedule(() => game?.GracefullyExit()));
.ContinueWith(_ => updateManager.Schedule(() => game?.AttemptExit()));
return true;
};
}

View File

@ -15,7 +15,6 @@ using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO.Archives;
using osu.Game.Models;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
@ -39,10 +38,7 @@ namespace osu.Game.Tests.Database
using (var importer = new BeatmapImporter(storage, realm))
using (new RealmRulesetStore(realm, storage))
{
Live<BeatmapSetInfo>? beatmapSet;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
beatmapSet = await importer.Import(reader);
var beatmapSet = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz"));
Assert.NotNull(beatmapSet);
Debug.Assert(beatmapSet != null);
@ -83,10 +79,7 @@ namespace osu.Game.Tests.Database
using (var importer = new BeatmapImporter(storage, realm))
using (new RealmRulesetStore(realm, storage))
{
Live<BeatmapSetInfo>? beatmapSet;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
beatmapSet = await importer.Import(reader);
var beatmapSet = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz"));
Assert.NotNull(beatmapSet);
Debug.Assert(beatmapSet != null);
@ -146,11 +139,8 @@ namespace osu.Game.Tests.Database
{
Task.Run(async () =>
{
Live<BeatmapSetInfo>? beatmapSet;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
// ReSharper disable once AccessToDisposedClosure
beatmapSet = await importer.Import(reader);
// ReSharper disable once AccessToDisposedClosure
var beatmapSet = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz"));
Assert.NotNull(beatmapSet);
Debug.Assert(beatmapSet != null);
@ -173,13 +163,8 @@ namespace osu.Game.Tests.Database
using (var importer = new BeatmapImporter(storage, realm))
using (new RealmRulesetStore(realm, storage))
{
Live<BeatmapSetInfo>? imported;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
{
imported = await importer.Import(reader);
EnsureLoaded(realm.Realm);
}
var imported = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz"));
EnsureLoaded(realm.Realm);
Assert.AreEqual(1, realm.Realm.All<BeatmapSetInfo>().Count());
@ -291,6 +276,42 @@ namespace osu.Game.Tests.Database
});
}
[Test]
public void TestImportDirectoryWithEmptyOsuFiles()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
string extractedFolder = $"{temp}_extracted";
Directory.CreateDirectory(extractedFolder);
try
{
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder);
foreach (var file in new DirectoryInfo(extractedFolder).GetFiles("*.osu"))
{
using (file.Open(FileMode.Create))
{
// empty file.
}
}
var imported = await importer.Import(new ImportTask(extractedFolder));
Assert.IsNull(imported);
}
finally
{
Directory.Delete(extractedFolder, true);
}
});
}
[Test]
public void TestImportThenImportWithReZip()
{
@ -760,7 +781,7 @@ namespace osu.Game.Tests.Database
}
};
var imported = importer.Import(toImport);
var imported = importer.ImportModel(toImport);
realm.Run(r => r.Refresh());

View File

@ -227,10 +227,10 @@ namespace osu.Game.Tests.Online
this.testBeatmapManager = testBeatmapManager;
}
public override Live<BeatmapSetInfo> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default)
public override Live<BeatmapSetInfo> ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default)
{
testBeatmapManager.AllowImport.Task.WaitSafely();
return (testBeatmapManager.CurrentImport = base.Import(item, archive, batchImport, cancellationToken));
return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, batchImport, cancellationToken));
}
}
}

View File

@ -15,7 +15,6 @@ using osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Skinning;
using SharpCompress.Archives.Zip;
@ -28,7 +27,7 @@ namespace osu.Game.Tests.Skins.IO
[Test]
public Task TestSingleImportDifferentFilename() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "skin.osk"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk"));
// When the import filename doesn't match, it should be appended (and update the skin.ini).
assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu);
@ -37,7 +36,7 @@ namespace osu.Game.Tests.Skins.IO
[Test]
public Task TestSingleImportWeirdIniFileCase() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner", iniFilename: "Skin.InI"), "skin.osk"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner", iniFilename: "Skin.InI"), "skin.osk"));
// When the import filename doesn't match, it should be appended (and update the skin.ini).
assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu);
@ -46,7 +45,7 @@ namespace osu.Game.Tests.Skins.IO
[Test]
public Task TestSingleImportMissingSectionHeader() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner", includeSectionHeader: false), "skin.osk"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner", includeSectionHeader: false), "skin.osk"));
// When the import filename doesn't match, it should be appended (and update the skin.ini).
assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu);
@ -55,7 +54,7 @@ namespace osu.Game.Tests.Skins.IO
[Test]
public Task TestSingleImportMatchingFilename() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "test skin.osk"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "test skin.osk"));
// When the import filename matches it shouldn't be appended.
assertCorrectMetadata(import1, "test skin", "skinner", osu);
@ -64,7 +63,7 @@ namespace osu.Game.Tests.Skins.IO
[Test]
public Task TestSingleImportNoIniFile() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithNonIniFile(), "test skin.osk"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithNonIniFile(), "test skin.osk"));
// When the import filename matches it shouldn't be appended.
assertCorrectMetadata(import1, "test skin", "Unknown", osu);
@ -73,7 +72,7 @@ namespace osu.Game.Tests.Skins.IO
[Test]
public Task TestEmptyImportImportsWithFilename() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createEmptyOsk(), "test skin.osk"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createEmptyOsk(), "test skin.osk"));
// When the import filename matches it shouldn't be appended.
assertCorrectMetadata(import1, "test skin", "Unknown", osu);
@ -86,8 +85,8 @@ namespace osu.Game.Tests.Skins.IO
[Test]
public Task TestImportTwiceWithSameMetadataAndFilename() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "skin.osk"));
var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "skin.osk"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk"));
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk"));
assertImportedOnce(import1, import2);
});
@ -96,8 +95,8 @@ namespace osu.Game.Tests.Skins.IO
public Task TestImportTwiceWithNoMetadataSameDownloadFilename() => runSkinTest(async osu =>
{
// if a user downloads two skins that do have skin.ini files but don't have any creator metadata in the skin.ini, they should both import separately just for safety.
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni(string.Empty, string.Empty), "download.osk"));
var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni(string.Empty, string.Empty), "download.osk"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni(string.Empty, string.Empty), "download.osk"));
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni(string.Empty, string.Empty), "download.osk"));
assertImportedOnce(import1, import2);
});
@ -105,10 +104,10 @@ namespace osu.Game.Tests.Skins.IO
[Test]
public Task TestImportUpperCasedOskArchive() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "name 1.OsK"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "name 1.OsK"));
assertCorrectMetadata(import1, "name 1", "author 1", osu);
var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "name 1.oSK"));
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "name 1.oSK"));
assertImportedOnce(import1, import2);
});
@ -118,7 +117,7 @@ namespace osu.Game.Tests.Skins.IO
{
MemoryStream exportStream = new MemoryStream();
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "custom.osk"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "custom.osk"));
assertCorrectMetadata(import1, "name 1 [custom]", "author 1", osu);
import1.PerformRead(s =>
@ -128,7 +127,7 @@ namespace osu.Game.Tests.Skins.IO
string exportFilename = import1.GetDisplayString();
var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(exportStream, $"{exportFilename}.osk"));
var import2 = await loadSkinIntoOsu(osu, new ImportTask(exportStream, $"{exportFilename}.osk"));
assertCorrectMetadata(import2, "name 1 [custom]", "author 1", osu);
assertImportedOnce(import1, import2);
@ -137,8 +136,8 @@ namespace osu.Game.Tests.Skins.IO
[Test]
public Task TestSameMetadataNameSameFolderName() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "my custom skin 1"));
var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "my custom skin 1"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1"));
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1"));
assertImportedOnce(import1, import2);
assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", osu);
@ -151,8 +150,8 @@ namespace osu.Game.Tests.Skins.IO
[Test]
public Task TestImportTwiceWithSameMetadataButDifferentFilename() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "skin.osk"));
var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "skin2.osk"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk"));
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin2.osk"));
assertImportedBoth(import1, import2);
});
@ -161,8 +160,8 @@ namespace osu.Game.Tests.Skins.IO
public Task TestImportTwiceWithNoMetadataDifferentDownloadFilename() => runSkinTest(async osu =>
{
// if a user downloads two skins that do have skin.ini files but don't have any creator metadata in the skin.ini, they should both import separately just for safety.
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni(string.Empty, string.Empty), "download.osk"));
var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni(string.Empty, string.Empty), "download2.osk"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni(string.Empty, string.Empty), "download.osk"));
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni(string.Empty, string.Empty), "download2.osk"));
assertImportedBoth(import1, import2);
});
@ -170,8 +169,8 @@ namespace osu.Game.Tests.Skins.IO
[Test]
public Task TestImportTwiceWithSameFilenameDifferentMetadata() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin v2", "skinner"), "skin.osk"));
var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin v2.1", "skinner"), "skin.osk"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin v2", "skinner"), "skin.osk"));
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin v2.1", "skinner"), "skin.osk"));
assertImportedBoth(import1, import2);
assertCorrectMetadata(import1, "test skin v2 [skin]", "skinner", osu);
@ -181,8 +180,8 @@ namespace osu.Game.Tests.Skins.IO
[Test]
public Task TestSameMetadataNameDifferentFolderName() => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "my custom skin 1"));
var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "my custom skin 2"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1"));
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 2"));
assertImportedBoth(import1, import2);
assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", osu);
@ -358,10 +357,10 @@ namespace osu.Game.Tests.Skins.IO
}
}
private async Task<Live<SkinInfo>> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null)
private async Task<Live<SkinInfo>> loadSkinIntoOsu(OsuGameBase osu, ImportTask import)
{
var skinManager = osu.Dependencies.Get<SkinManager>();
return await skinManager.Import(archive);
return await skinManager.Import(import);
}
}
}

View File

@ -10,7 +10,7 @@ using osu.Framework.Extensions;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.IO.Archives;
using osu.Game.Database;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
@ -27,7 +27,7 @@ namespace osu.Game.Tests.Skins
[BackgroundDependencyLoader]
private void load()
{
var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).GetResultSafely();
var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource("Archives/ogg-beatmap.osz"), "ogg-beatmap.osz")).GetResultSafely();
imported?.PerformRead(s =>
{

View File

@ -8,7 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.IO.Archives;
using osu.Game.Database;
using osu.Game.Skinning;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
@ -26,7 +26,7 @@ namespace osu.Game.Tests.Skins
[BackgroundDependencyLoader]
private void load()
{
var imported = skins.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-skin.osk"))).GetResultSafely();
var imported = skins.Import(new ImportTask(TestResources.OpenResource("Archives/ogg-skin.osk"), "ogg-skin.osk")).GetResultSafely();
skin = imported.PerformRead(skinInfo => skins.GetSkin(skinInfo));
}

View File

@ -0,0 +1,57 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Select;
using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorNavigation : OsuGameTestScene
{
[Test]
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
{
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("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.IsLoaded);
AddStep("test gameplay", () =>
{
var testGameplayButton = this.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(testGameplayButton);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen is EditorPlayer editorPlayer && editorPlayer.IsLoaded);
AddAssert("current ruleset is osu!", () => Game.Ruleset.Value.Equals(new OsuRuleset().RulesetInfo));
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield()));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddAssert("previous ruleset restored", () => Game.Ruleset.Value.Equals(new ManiaRuleset().RulesetInfo));
}
}
}

View File

@ -8,7 +8,9 @@ using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
@ -16,6 +18,7 @@ using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning.Editor;
using osu.Game.Tests.Beatmaps.IO;
using osuTK;
using osuTK.Input;
using static osu.Game.Tests.Visual.Navigation.TestSceneScreenNavigation;
@ -26,29 +29,6 @@ namespace osu.Game.Tests.Visual.Navigation
private TestPlaySongSelect songSelect;
private SkinEditor skinEditor => Game.ChildrenOfType<SkinEditor>().FirstOrDefault();
private void advanceToSongSelect()
{
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
}
private void openSkinEditor()
{
AddStep("open skin editor", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.S);
InputManager.ReleaseKey(Key.ControlLeft);
InputManager.ReleaseKey(Key.ShiftLeft);
});
AddUntilStep("skin editor loaded", () => skinEditor != null);
}
[Test]
public void TestEditComponentDuringGameplay()
{
@ -88,6 +68,68 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default);
}
[Test]
public void TestComponentsDeselectedOnSkinEditorHide()
{
advanceToSongSelect();
openSkinEditor();
switchToGameplayScene();
AddUntilStep("wait for components", () => skinEditor.ChildrenOfType<SkinBlueprint>().Any());
AddStep("select all components", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.A);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("components selected", () => skinEditor.SelectedComponents.Count > 0);
toggleSkinEditor();
AddUntilStep("no components selected", () => skinEditor.SelectedComponents.Count == 0);
}
[Test]
public void TestSwitchScreenWhileDraggingComponent()
{
Vector2 firstBlueprintCentre = Vector2.Zero;
ScheduledDelegate movementDelegate = null;
advanceToSongSelect();
openSkinEditor();
AddStep("add skinnable component", () =>
{
skinEditor.ChildrenOfType<SkinComponentToolbox.ToolboxComponentButton>().First().TriggerClick();
});
AddUntilStep("newly added component selected", () => skinEditor.SelectedComponents.Count == 1);
AddStep("start drag", () =>
{
firstBlueprintCentre = skinEditor.ChildrenOfType<SkinBlueprint>().First().ScreenSpaceDrawQuad.Centre;
InputManager.MoveMouseTo(firstBlueprintCentre);
InputManager.PressButton(MouseButton.Left);
});
AddStep("start movement", () => movementDelegate = Scheduler.AddDelayed(() => { InputManager.MoveMouseTo(firstBlueprintCentre += new Vector2(1)); }, 10, true));
toggleSkinEditor();
AddStep("exit song select", () => songSelect.Exit());
AddUntilStep("wait for blueprints removed", () => !skinEditor.ChildrenOfType<SkinBlueprint>().Any());
AddStep("stop drag", () =>
{
InputManager.ReleaseButton(MouseButton.Left);
movementDelegate?.Cancel();
});
}
[Test]
public void TestAutoplayCompatibleModsRetainedOnEnteringGameplay()
{
@ -146,8 +188,35 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("mod overlay closed", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden);
}
private void advanceToSongSelect()
{
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
}
private void openSkinEditor()
{
toggleSkinEditor();
AddUntilStep("skin editor loaded", () => skinEditor != null);
}
private void toggleSkinEditor()
{
AddStep("toggle skin editor", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.S);
InputManager.ReleaseKey(Key.ControlLeft);
InputManager.ReleaseKey(Key.ShiftLeft);
});
}
private void switchToGameplayScene()
{
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("Click gameplay scene button", () =>
{
InputManager.MoveMouseTo(skinEditor.ChildrenOfType<SkinEditorSceneLibrary.SceneButton>().First(b => b.Text == "Gameplay"));

View File

@ -27,7 +27,7 @@ namespace osu.Game.Tournament.Screens.Setup
dropdown.Items = storage.ListTournaments();
dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true);
Action = () => game.GracefullyExit();
Action = () => game.AttemptExit();
folderButton.Action = () => storage.PresentExternally();
ButtonText = "Close osu!";

View File

@ -177,8 +177,17 @@ namespace osu.Game.Beatmaps
}
Beatmap beatmap;
using (var stream = new LineBufferedReader(reader.GetStream(mapName)))
{
if (stream.PeekLine() == null)
{
Logger.Log($"No content found in first .osu file of beatmap archive ({reader.Name} / {mapName})", LoggingTarget.Database);
return null;
}
beatmap = Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
}
return new BeatmapSetInfo
{

View File

@ -101,7 +101,7 @@ namespace osu.Game.Beatmaps
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
b.BeatmapSet = beatmapSet;
var imported = beatmapImporter.Import(beatmapSet);
var imported = beatmapImporter.ImportModel(beatmapSet);
if (imported == null)
throw new InvalidOperationException("Failed to import new beatmap");
@ -409,11 +409,8 @@ namespace osu.Game.Beatmaps
public Task<Live<BeatmapSetInfo>?> Import(ImportTask task, bool batchImport = false, CancellationToken cancellationToken = default) =>
beatmapImporter.Import(task, batchImport, cancellationToken);
public Task<Live<BeatmapSetInfo>?> Import(ArchiveReader archive, bool batchImport = false, CancellationToken cancellationToken = default) =>
beatmapImporter.Import(archive, batchImport, cancellationToken);
public Live<BeatmapSetInfo>? Import(BeatmapSetInfo item, ArchiveReader? archive = null, CancellationToken cancellationToken = default) =>
beatmapImporter.Import(item, archive, false, cancellationToken);
beatmapImporter.ImportModel(item, archive, false, cancellationToken);
public IEnumerable<string> HandledExtensions => beatmapImporter.HandledExtensions;
@ -457,9 +454,9 @@ namespace osu.Game.Beatmaps
#region Implementation of IPostImports<out BeatmapSetInfo>
public Action<IEnumerable<Live<BeatmapSetInfo>>>? PostImport
public Action<IEnumerable<Live<BeatmapSetInfo>>>? PresentImport
{
set => beatmapImporter.PostImport = value;
set => beatmapImporter.PresentImport = value;
}
#endregion

View File

@ -74,7 +74,7 @@ namespace osu.Game.Beatmaps.Formats
}
if (line == null)
throw new IOException("Unknown file format (null)");
throw new IOException("Unknown file format (no content)");
var decoder = typedDecoders.Where(d => line.StartsWith(d.Key, StringComparison.InvariantCulture)).Select(d => d.Value).FirstOrDefault();

View File

@ -1,6 +1,7 @@
// 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.Collections.Generic;
using System.Threading.Tasks;
using osu.Game.Overlays.Notifications;
@ -11,7 +12,7 @@ namespace osu.Game.Database
/// A class which handles importing of associated models to the game store.
/// </summary>
/// <typeparam name="TModel">The model type.</typeparam>
public interface IModelImporter<TModel> : IPostNotifications, IPostImports<TModel>, ICanAcceptFiles
public interface IModelImporter<TModel> : IPostNotifications, ICanAcceptFiles
where TModel : class, IHasGuidPrimaryKey
{
/// <summary>
@ -26,5 +27,10 @@ namespace osu.Game.Database
/// A user displayable name for the model type associated with this manager.
/// </summary>
string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}";
/// <summary>
/// Fired when the user requests to view the resulting import.
/// </summary>
public Action<IEnumerable<Live<TModel>>>? PresentImport { set; }
}
}

View File

@ -1,17 +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.
using System;
using System.Collections.Generic;
namespace osu.Game.Database
{
public interface IPostImports<TModel>
where TModel : class, IHasGuidPrimaryKey
{
/// <summary>
/// Fired when the user requests to view the resulting import.
/// </summary>
public Action<IEnumerable<Live<TModel>>>? PostImport { set; }
}
}

View File

@ -33,7 +33,7 @@ namespace osu.Game.Database
}
/// <summary>
/// Construct a new import task from a stream.
/// Construct a new import task from a stream. The provided stream will be disposed after reading.
/// </summary>
public ImportTask(Stream stream, string filename)
{
@ -62,6 +62,7 @@ namespace osu.Game.Database
{
// This isn't used in any current path. May need to reconsider for performance reasons (ie. if we don't expect the incoming stream to be copied out).
memoryStream = new MemoryStream(stream.ReadAllBytesToArray());
stream.Dispose();
}
if (ZipUtils.IsZipArchive(memoryStream))

View File

@ -56,7 +56,7 @@ namespace osu.Game.Database
/// </summary>
private static readonly ThreadedTaskScheduler import_scheduler_batch = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(RealmArchiveModelImporter<TModel>));
public virtual IEnumerable<string> HandledExtensions => new[] { @".zip" };
public abstract IEnumerable<string> HandledExtensions { get; }
protected readonly RealmFileStore Files;
@ -65,7 +65,7 @@ namespace osu.Game.Database
/// <summary>
/// Fired when the user requests to view the resulting import.
/// </summary>
public Action<IEnumerable<Live<TModel>>>? PostImport { get; set; }
public Action<IEnumerable<Live<TModel>>>? PresentImport { get; set; }
/// <summary>
/// Set an endpoint for notifications to be posted to.
@ -158,12 +158,12 @@ namespace osu.Game.Database
? $"Imported {imported.First().GetDisplayString()}!"
: $"Imported {imported.Count} {HumanisedModelName}s!";
if (imported.Count > 0 && PostImport != null)
if (imported.Count > 0 && PresentImport != null)
{
notification.CompletionText += " Click to view.";
notification.CompletionClickAction = () =>
{
PostImport?.Invoke(imported);
PresentImport?.Invoke(imported);
return true;
};
}
@ -188,7 +188,7 @@ namespace osu.Game.Database
Live<TModel>? import;
using (ArchiveReader reader = task.GetReader())
import = await Import(reader, batchImport, cancellationToken).ConfigureAwait(false);
import = await importFromArchive(reader, batchImport, cancellationToken).ConfigureAwait(false);
// We may or may not want to delete the file depending on where it is stored.
// e.g. reconstructing/repairing database with items from default storage.
@ -208,12 +208,15 @@ namespace osu.Game.Database
}
/// <summary>
/// Silently import an item from an <see cref="ArchiveReader"/>.
/// Create and import a model based off the provided <see cref="ArchiveReader"/>.
/// </summary>
/// <remarks>
/// This method also handled queueing the import task on a relevant import thread pool.
/// </remarks>
/// <param name="archive">The archive to be imported.</param>
/// <param name="batchImport">Whether this import is part of a larger batch.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
public async Task<Live<TModel>?> Import(ArchiveReader archive, bool batchImport = false, CancellationToken cancellationToken = default)
private async Task<Live<TModel>?> importFromArchive(ArchiveReader archive, bool batchImport = false, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
@ -236,7 +239,7 @@ namespace osu.Game.Database
return null;
}
var scheduledImport = Task.Factory.StartNew(() => Import(model, archive, batchImport, cancellationToken),
var scheduledImport = Task.Factory.StartNew(() => ImportModel(model, archive, batchImport, cancellationToken),
cancellationToken,
TaskCreationOptions.HideScheduler,
batchImport ? import_scheduler_batch : import_scheduler);
@ -251,7 +254,7 @@ namespace osu.Game.Database
/// <param name="archive">An optional archive to use for model population.</param>
/// <param name="batchImport">If <c>true</c>, imports will be skipped before they begin, given an existing model matches on hash and filenames. Should generally only be used for large batch imports, as it may defy user expectations when updating an existing model.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
public virtual Live<TModel>? Import(TModel item, ArchiveReader? archive = null, bool batchImport = false, CancellationToken cancellationToken = default) => Realm.Run(realm =>
public virtual Live<TModel>? ImportModel(TModel item, ArchiveReader? archive = null, bool batchImport = false, CancellationToken cancellationToken = default) => Realm.Run(realm =>
{
cancellationToken.ThrowIfCancellationRequested();
@ -335,6 +338,8 @@ namespace osu.Game.Database
transaction.Commit();
}
PostImport(item, realm);
LogForModel(item, @"Import successfully completed!");
}
catch (Exception e)
@ -470,6 +475,15 @@ namespace osu.Game.Database
{
}
/// <summary>
/// Perform any final actions after the import has been committed to the database.
/// </summary>
/// <param name="model">The model prepared for import.</param>
/// <param name="realm">The current realm context.</param>
protected virtual void PostImport(TModel model, Realm realm)
{
}
/// <summary>
/// Check whether an existing model already exists for a new import item.
/// </summary>

View File

@ -637,6 +637,12 @@ namespace osu.Game
Add(performFromMainMenuTask = new PerformFromMenuRunner(action, validScreens, () => ScreenStack.CurrentScreen));
}
public override void AttemptExit()
{
// Using PerformFromScreen gives the user a chance to interrupt the exit process if needed.
PerformFromScreen(menu => menu.Exit());
}
/// <summary>
/// Wait for the game (and target component) to become loaded and then run an action.
/// </summary>
@ -705,13 +711,13 @@ namespace osu.Game
SkinManager.PostNotification = n => Notifications.Post(n);
BeatmapManager.PostNotification = n => Notifications.Post(n);
BeatmapManager.PostImport = items => PresentBeatmap(items.First().Value);
BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value);
BeatmapDownloader.PostNotification = n => Notifications.Post(n);
ScoreDownloader.PostNotification = n => Notifications.Post(n);
ScoreManager.PostNotification = n => Notifications.Post(n);
ScoreManager.PostImport = items => PresentScore(items.First().Value);
ScoreManager.PresentImport = items => PresentScore(items.First().Value);
// make config aware of how to lookup skins for on-screen display purposes.
// if this becomes a more common thing, tracked settings should be reconsidered to allow local DI.

View File

@ -264,14 +264,16 @@ namespace osu.Game
var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
dependencies.Cache(difficultyCache = new BeatmapDifficultyCache());
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, () => difficultyCache, LocalConfig));
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, difficultyCache, LocalConfig));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true));
dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API));
dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API));
dependencies.Cache(difficultyCache = new BeatmapDifficultyCache());
// Add after all the above cache operations as it depends on them.
AddInternal(difficultyCache);
dependencies.Cache(userCache = new UserLookupCache());
@ -417,14 +419,15 @@ namespace osu.Game
/// <summary>
/// Use to programatically exit the game as if the user was triggering via alt-f4.
/// Will keep persisting until an exit occurs (exit may be blocked multiple times).
/// By default, will keep persisting until an exit occurs (exit may be blocked multiple times).
/// May be interrupted (see <see cref="OsuGame"/>'s override).
/// </summary>
public void GracefullyExit()
public virtual void AttemptExit()
{
if (!OnExiting())
Exit();
else
Scheduler.AddDelayed(GracefullyExit, 2000);
Scheduler.AddDelayed(AttemptExit, 2000);
}
public bool Migrate(string path)

View File

@ -56,7 +56,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
dialogOverlay.Push(new ConfirmDialog("To complete this operation, osu! will close. Please open it again to use the new data location.", () =>
{
(storage as OsuStorage)?.ChangeDataPath(target.FullName);
game.GracefullyExit();
game.Exit();
}, () => { }));
},
() => { }));

View File

@ -27,16 +27,16 @@ namespace osu.Game.Scoring
public class ScoreManager : ModelManager<ScoreInfo>, IModelImporter<ScoreInfo>
{
private readonly Scheduler scheduler;
private readonly Func<BeatmapDifficultyCache> difficulties;
private readonly BeatmapDifficultyCache difficultyCache;
private readonly OsuConfigManager configManager;
private readonly ScoreImporter scoreImporter;
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmAccess realm, Scheduler scheduler,
Func<BeatmapDifficultyCache> difficulties = null, OsuConfigManager configManager = null)
BeatmapDifficultyCache difficultyCache = null, OsuConfigManager configManager = null)
: base(storage, realm)
{
this.scheduler = scheduler;
this.difficulties = difficulties;
this.difficultyCache = difficultyCache;
this.configManager = configManager;
scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm)
@ -65,8 +65,6 @@ namespace osu.Game.Scoring
/// <returns>The given <paramref name="scores"/> ordered by decreasing total score.</returns>
public async Task<ScoreInfo[]> OrderByTotalScoreAsync(ScoreInfo[] scores, CancellationToken cancellationToken = default)
{
var difficultyCache = difficulties?.Invoke();
if (difficultyCache != null)
{
// Compute difficulties asynchronously first to prevent blocking via the GetTotalScore() call below.
@ -168,11 +166,11 @@ namespace osu.Game.Scoring
return score.BeatmapInfo.MaxCombo.Value;
#pragma warning restore CS0618
if (difficulties == null)
if (difficultyCache == null)
return null;
// We can compute the max combo locally after the async beatmap difficulty computation.
var difficulty = await difficulties().GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
var difficulty = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
return difficulty?.MaxCombo;
}
@ -265,13 +263,13 @@ namespace osu.Game.Scoring
public Task<IEnumerable<Live<ScoreInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => scoreImporter.Import(notification, tasks);
public Live<ScoreInfo> Import(ScoreInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default) =>
scoreImporter.Import(item, archive, batchImport, cancellationToken);
scoreImporter.ImportModel(item, archive, batchImport, cancellationToken);
#region Implementation of IPresentImports<ScoreInfo>
public Action<IEnumerable<Live<ScoreInfo>>> PostImport
public Action<IEnumerable<Live<ScoreInfo>>> PresentImport
{
set => scoreImporter.PostImport = value;
set => scoreImporter.PresentImport = value;
}
#endregion

View File

@ -65,6 +65,8 @@ namespace osu.Game.Screens.Edit
base.LoadComplete();
// will be restored via lease, see `DisallowExternalBeatmapRulesetChanges`.
if (!(Beatmap.Value is DummyWorkingBeatmap))
Ruleset.Value = Beatmap.Value.BeatmapInfo.Ruleset;
Mods.Value = Array.Empty<Mod>();
}

View File

@ -19,7 +19,6 @@ using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
@ -141,7 +140,7 @@ namespace osu.Game.Screens.Menu
{
// if we detect that the theme track or beatmap is unavailable this is either first startup or things are in a bad state.
// this could happen if a user has nuked their files store. for now, reimport to repair this.
var import = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream($"Tracks/{BeatmapFile}"), BeatmapFile)).GetResultSafely();
var import = beatmaps.Import(new ImportTask(game.Resources.GetStream($"Tracks/{BeatmapFile}"), BeatmapFile)).GetResultSafely();
import?.PerformWrite(b => b.Protected = true);

View File

@ -786,7 +786,17 @@ namespace osu.Game.Screens.Select
Ruleset.ValueChanged += r => updateSelectedRuleset(r.NewValue);
decoupledRuleset.ValueChanged += r => Ruleset.Value = r.NewValue;
decoupledRuleset.ValueChanged += r =>
{
bool wasDisabled = Ruleset.Disabled;
// a sub-screen may have taken a lease on this decoupled ruleset bindable,
// which would indirectly propagate to the game-global bindable via the `DisabledChanged` callback below.
// to make sure changes sync without crashes, lift the disable for a short while to sync, and then restore the old value.
Ruleset.Disabled = false;
Ruleset.Value = r.NewValue;
Ruleset.Disabled = wasDisabled;
};
decoupledRuleset.DisabledChanged += r => Ruleset.Disabled = r;
Beatmap.BindValueChanged(workingBeatmapChanged);

View File

@ -88,7 +88,7 @@ namespace osu.Game.Skinning.Editor
}
}
private class ToolboxComponentButton : OsuButton
public class ToolboxComponentButton : OsuButton
{
protected override bool ShouldBeConsideredForInput(Drawable child) => false;

View File

@ -322,6 +322,12 @@ namespace osu.Game.Skinning.Editor
protected override bool OnMouseDown(MouseDownEvent e) => true;
public override void Hide()
{
base.Hide();
SelectedComponents.Clear();
}
protected override void PopIn()
{
this

View File

@ -24,7 +24,6 @@ using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Overlays.Notifications;
using osu.Game.Utils;
@ -168,7 +167,7 @@ namespace osu.Game.Skinning
Name = NamingUtils.GetNextBestName(existingSkinNames, $@"{s.Name} (modified)")
};
var result = skinImporter.Import(skinInfo);
var result = skinImporter.ImportModel(skinInfo);
if (result != null)
{
@ -260,9 +259,9 @@ namespace osu.Game.Skinning
#region Implementation of IModelImporter<SkinInfo>
public Action<IEnumerable<Live<SkinInfo>>> PostImport
public Action<IEnumerable<Live<SkinInfo>>> PresentImport
{
set => skinImporter.PostImport = value;
set => skinImporter.PresentImport = value;
}
public Task Import(params string[] paths) => skinImporter.Import(paths);
@ -275,9 +274,6 @@ namespace osu.Game.Skinning
public Task<Live<SkinInfo>> Import(ImportTask task, bool batchImport = false, CancellationToken cancellationToken = default) => skinImporter.Import(task, batchImport, cancellationToken);
public Task<Live<SkinInfo>> Import(ArchiveReader archive, bool batchImport = false, CancellationToken cancellationToken = default) =>
skinImporter.Import(archive, batchImport, cancellationToken);
#endregion
public void Delete([CanBeNull] Expression<Func<SkinInfo, bool>> filter = null, bool silent = false)