mirror of
https://github.com/ppy/osu
synced 2025-01-03 12:52:10 +00:00
Merge branch 'master' into first-run-setup-notification-interruption
This commit is contained in:
commit
9c4f6d2ce0
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -82,7 +82,7 @@ jobs:
|
||||
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
|
||||
|
||||
- name: Test
|
||||
run: dotnet test $pwd/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx"
|
||||
run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx"
|
||||
shell: pwsh
|
||||
|
||||
# Attempt to upload results even if test fails.
|
||||
|
@ -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 osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -41,7 +42,7 @@ namespace osu.Game.Rulesets.EmptyFreeform
|
||||
return new[] { new EmptyFreeformModAutoplay() };
|
||||
|
||||
default:
|
||||
return new Mod[] { null };
|
||||
return Array.Empty<Mod>();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@ -37,7 +38,7 @@ namespace osu.Game.Rulesets.Pippidon
|
||||
return new[] { new PippidonModAutoplay() };
|
||||
|
||||
default:
|
||||
return new Mod[] { null };
|
||||
return Array.Empty<Mod>();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@ -34,7 +35,7 @@ namespace osu.Game.Rulesets.EmptyScrolling
|
||||
return new[] { new EmptyScrollingModAutoplay() };
|
||||
|
||||
default:
|
||||
return new Mod[] { null };
|
||||
return Array.Empty<Mod>();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@ -34,7 +35,7 @@ namespace osu.Game.Rulesets.Pippidon
|
||||
return new[] { new PippidonModAutoplay() };
|
||||
|
||||
default:
|
||||
return new Mod[] { null };
|
||||
return Array.Empty<Mod>();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,8 +51,8 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.611.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.616.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.615.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
|
@ -1,6 +1,8 @@
|
||||
// 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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@ -12,7 +14,6 @@ using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
@ -20,7 +21,6 @@ using osu.Game.IO.Archives;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Stores;
|
||||
using osu.Game.Tests.Resources;
|
||||
using Realms;
|
||||
using SharpCompress.Archives;
|
||||
@ -28,8 +28,6 @@ using SharpCompress.Archives.Zip;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Writers.Zip;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
{
|
||||
[TestFixture]
|
||||
@ -40,7 +38,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using (var importer = new BeatmapModelManager(realm, storage))
|
||||
using (var importer = new BeatmapImporter(storage, realm))
|
||||
using (new RealmRulesetStore(realm, storage))
|
||||
{
|
||||
Live<BeatmapSetInfo>? beatmapSet;
|
||||
@ -84,7 +82,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using (var importer = new BeatmapModelManager(realm, storage))
|
||||
using (var importer = new BeatmapImporter(storage, realm))
|
||||
using (new RealmRulesetStore(realm, storage))
|
||||
{
|
||||
Live<BeatmapSetInfo>? beatmapSet;
|
||||
@ -143,7 +141,9 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
BeatmapSetInfo? detachedSet = null;
|
||||
|
||||
using (var importer = new BeatmapModelManager(realm, storage))
|
||||
var manager = new ModelManager<BeatmapSetInfo>(storage, realm);
|
||||
|
||||
using (var importer = new BeatmapImporter(storage, realm))
|
||||
using (new RealmRulesetStore(realm, storage))
|
||||
{
|
||||
Task.Run(async () =>
|
||||
@ -162,7 +162,7 @@ namespace osu.Game.Tests.Database
|
||||
}).WaitSafely();
|
||||
|
||||
Debug.Assert(detachedSet != null);
|
||||
importer.AddFile(detachedSet, new MemoryStream(), "test");
|
||||
manager.AddFile(detachedSet, new MemoryStream(), "test");
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -172,7 +172,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using (var importer = new BeatmapModelManager(realm, storage))
|
||||
using (var importer = new BeatmapImporter(storage, realm))
|
||||
using (new RealmRulesetStore(realm, storage))
|
||||
{
|
||||
Live<BeatmapSetInfo>? imported;
|
||||
@ -204,7 +204,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
await LoadOszIntoStore(importer, realm.Realm);
|
||||
@ -216,7 +216,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
@ -234,7 +234,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
@ -248,7 +248,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? tempPath = TestResources.GetTestBeatmapForImport();
|
||||
@ -278,7 +278,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
@ -298,7 +298,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
@ -347,7 +347,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
@ -394,12 +394,11 @@ namespace osu.Game.Tests.Database
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("intentionally broken by import optimisations")]
|
||||
public void TestImportThenImportWithChangedFile()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
@ -447,7 +446,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
@ -491,28 +490,29 @@ namespace osu.Game.Tests.Database
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("intentionally broken by import optimisations")]
|
||||
public void TestImportCorruptThenImport()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
|
||||
var firstFile = imported.Files.First();
|
||||
|
||||
var fileStorage = storage.GetStorageForDirectory("files");
|
||||
|
||||
long originalLength;
|
||||
using (var stream = storage.GetStream(firstFile.File.GetStoragePath()))
|
||||
using (var stream = fileStorage.GetStream(firstFile.File.GetStoragePath()))
|
||||
originalLength = stream.Length;
|
||||
|
||||
using (var stream = storage.CreateFileSafely(firstFile.File.GetStoragePath()))
|
||||
using (var stream = fileStorage.CreateFileSafely(firstFile.File.GetStoragePath()))
|
||||
stream.WriteByte(0);
|
||||
|
||||
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
|
||||
|
||||
using (var stream = storage.GetStream(firstFile.File.GetStoragePath()))
|
||||
using (var stream = fileStorage.GetStream(firstFile.File.GetStoragePath()))
|
||||
Assert.AreEqual(stream.Length, originalLength, "Corruption was not fixed on second import");
|
||||
|
||||
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
|
||||
@ -529,7 +529,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var progressNotification = new ImportProgressNotification();
|
||||
@ -567,7 +567,7 @@ namespace osu.Game.Tests.Database
|
||||
Interlocked.Increment(ref loggedExceptionCount);
|
||||
};
|
||||
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
@ -619,10 +619,10 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm, batchImport: true);
|
||||
|
||||
deleteBeatmapSet(imported, realm.Realm);
|
||||
|
||||
@ -646,7 +646,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realmFactory, storage);
|
||||
using var importer = new BeatmapImporter(storage, realmFactory);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realmFactory.Realm);
|
||||
@ -678,7 +678,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new NonOptimisedBeatmapImporter(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
@ -705,7 +705,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
@ -731,7 +731,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealm((realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var metadata = new BeatmapMetadata
|
||||
@ -779,7 +779,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
@ -796,7 +796,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
@ -832,7 +832,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
@ -874,7 +874,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
@ -925,7 +925,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapModelManager(realm, storage);
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
@ -960,11 +960,11 @@ namespace osu.Game.Tests.Database
|
||||
return realm.All<BeatmapSetInfo>().FirstOrDefault(beatmapSet => beatmapSet.ID == importedSet!.ID);
|
||||
}
|
||||
|
||||
public static async Task<BeatmapSetInfo> LoadOszIntoStore(BeatmapImporter importer, Realm realm, string? path = null, bool virtualTrack = false)
|
||||
public static async Task<BeatmapSetInfo> LoadOszIntoStore(BeatmapImporter importer, Realm realm, string? path = null, bool virtualTrack = false, bool batchImport = false)
|
||||
{
|
||||
string? temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack);
|
||||
|
||||
var importedSet = await importer.Import(new ImportTask(temp));
|
||||
var importedSet = await importer.Import(new ImportTask(temp), batchImport);
|
||||
|
||||
Assert.NotNull(importedSet);
|
||||
Debug.Assert(importedSet != null);
|
||||
@ -1081,15 +1081,5 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
Assert.Fail(failureMessage);
|
||||
}
|
||||
|
||||
public class NonOptimisedBeatmapImporter : BeatmapImporter
|
||||
{
|
||||
public NonOptimisedBeatmapImporter(RealmAccess realm, Storage storage)
|
||||
: base(realm, storage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool HasCustomHashFunction => true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
// 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 System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Stores;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
{
|
||||
|
73
osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs
Normal file
73
osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs
Normal file
@ -0,0 +1,73 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO;
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
{
|
||||
[TestFixture]
|
||||
public class LegacyBeatmapImporterTest
|
||||
{
|
||||
private readonly TestLegacyBeatmapImporter importer = new TestLegacyBeatmapImporter();
|
||||
|
||||
[Test]
|
||||
public void TestSongsSubdirectories()
|
||||
{
|
||||
using (var storage = new TemporaryNativeStorage("stable-songs-folder"))
|
||||
{
|
||||
var songsStorage = storage.GetStorageForDirectory(StableStorage.STABLE_DEFAULT_SONGS_PATH);
|
||||
|
||||
// normal beatmap folder
|
||||
var beatmap1 = songsStorage.GetStorageForDirectory("beatmap1");
|
||||
createFile(beatmap1, "beatmap.osu");
|
||||
|
||||
// songs subdirectory
|
||||
var subdirectory = songsStorage.GetStorageForDirectory("subdirectory");
|
||||
createFile(subdirectory, Path.Combine("beatmap2", "beatmap.osu"));
|
||||
createFile(subdirectory, Path.Combine("beatmap3", "beatmap.osu"));
|
||||
createFile(subdirectory, Path.Combine("sub-subdirectory", "beatmap4", "beatmap.osu"));
|
||||
|
||||
// songs subdirectory with system file
|
||||
var subdirectory2 = songsStorage.GetStorageForDirectory("subdirectory2");
|
||||
createFile(subdirectory2, ".DS_Store");
|
||||
createFile(subdirectory2, Path.Combine("beatmap5", "beatmap.osu"));
|
||||
createFile(subdirectory2, Path.Combine("beatmap6", "beatmap.osu"));
|
||||
|
||||
// empty songs subdirectory
|
||||
songsStorage.GetStorageForDirectory("subdirectory3");
|
||||
|
||||
string[] paths = importer.GetStableImportPaths(songsStorage).ToArray();
|
||||
Assert.That(paths.Length, Is.EqualTo(6));
|
||||
Assert.That(paths.Contains(songsStorage.GetFullPath("beatmap1")));
|
||||
Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory", "beatmap2"))));
|
||||
Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory", "beatmap3"))));
|
||||
Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory", "sub-subdirectory", "beatmap4"))));
|
||||
Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory2", "beatmap5"))));
|
||||
Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory2", "beatmap6"))));
|
||||
}
|
||||
|
||||
static void createFile(Storage storage, string path)
|
||||
{
|
||||
using (var stream = storage.CreateFileSafely(path))
|
||||
stream.WriteByte(0);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestLegacyBeatmapImporter : LegacyBeatmapImporter
|
||||
{
|
||||
public TestLegacyBeatmapImporter()
|
||||
: base(null)
|
||||
{
|
||||
}
|
||||
|
||||
public new IEnumerable<string> GetStableImportPaths(Storage storage) => base.GetStableImportPaths(storage);
|
||||
}
|
||||
}
|
||||
}
|
@ -210,25 +210,25 @@ namespace osu.Game.Tests.Online
|
||||
{
|
||||
}
|
||||
|
||||
protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue)
|
||||
protected override BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue)
|
||||
{
|
||||
return new TestBeatmapModelManager(this, storage, realm, onlineLookupQueue);
|
||||
return new TestBeatmapImporter(this, storage, realm, onlineLookupQueue);
|
||||
}
|
||||
|
||||
internal class TestBeatmapModelManager : BeatmapModelManager
|
||||
internal class TestBeatmapImporter : BeatmapImporter
|
||||
{
|
||||
private readonly TestBeatmapManager testBeatmapManager;
|
||||
|
||||
public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue)
|
||||
: base(databaseAccess, storage, beatmapOnlineLookupQueue)
|
||||
public TestBeatmapImporter(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue)
|
||||
: base(storage, databaseAccess, beatmapOnlineLookupQueue)
|
||||
{
|
||||
this.testBeatmapManager = testBeatmapManager;
|
||||
}
|
||||
|
||||
public override Live<BeatmapSetInfo> Import(BeatmapSetInfo item, ArchiveReader archive = null, CancellationToken cancellationToken = default)
|
||||
public override Live<BeatmapSetInfo> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
testBeatmapManager.AllowImport.Task.WaitSafely();
|
||||
return (testBeatmapManager.CurrentImport = base.Import(item, archive, cancellationToken));
|
||||
return (testBeatmapManager.CurrentImport = base.Import(item, archive, batchImport, cancellationToken));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
81
osu.Game.Tests/Rulesets/TestSceneBrokenRulesetHandling.cs
Normal file
81
osu.Game.Tests/Rulesets/TestSceneBrokenRulesetHandling.cs
Normal file
@ -0,0 +1,81 @@
|
||||
// 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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Tests.Rulesets
|
||||
{
|
||||
[HeadlessTest]
|
||||
public class TestSceneBrokenRulesetHandling : OsuTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private OsuGameBase gameBase { get; set; } = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("reset ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNullModsReturnedByRulesetAreIgnored()
|
||||
{
|
||||
AddStep("set ruleset with null mods", () => Ruleset.Value = new TestRulesetWithNullMods().RulesetInfo);
|
||||
AddAssert("no null mods in available mods", () => gameBase.AvailableMods.Value.SelectMany(kvp => kvp.Value).All(mod => mod != null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRulesetRevertedIfModsCannotBeRetrieved()
|
||||
{
|
||||
RulesetInfo ruleset = null!;
|
||||
|
||||
AddStep("store current ruleset", () => ruleset = Ruleset.Value);
|
||||
|
||||
AddStep("set API incompatible ruleset", () => Ruleset.Value = new TestAPIIncompatibleRuleset().RulesetInfo);
|
||||
AddAssert("ruleset not changed", () => Ruleset.Value.Equals(ruleset));
|
||||
}
|
||||
|
||||
#nullable disable // purposefully disabling nullability to simulate broken or unannotated API user code.
|
||||
|
||||
private class TestRulesetWithNullMods : Ruleset
|
||||
{
|
||||
public override string ShortName => "nullmods";
|
||||
public override string Description => "nullmods";
|
||||
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[] { null };
|
||||
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => null;
|
||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null;
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null;
|
||||
}
|
||||
|
||||
private class TestAPIIncompatibleRuleset : Ruleset
|
||||
{
|
||||
public override string ShortName => "incompatible";
|
||||
public override string Description => "incompatible";
|
||||
|
||||
// simulate API incompatibility by throwing similar exceptions.
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new MissingMethodException();
|
||||
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => null;
|
||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null;
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null;
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@ -61,6 +62,21 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddUntilStep("context menu is visible", () => contextMenuContainer.ChildrenOfType<OsuContextMenu>().Single().State == MenuState.Open);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectAndShowContextMenuOutsideBounds()
|
||||
{
|
||||
var addedObject = new HitCircle { StartTime = 100, Position = OsuPlayfield.BASE_SIZE };
|
||||
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
|
||||
|
||||
AddStep("descale blueprint container", () => this.ChildrenOfType<HitObjectComposer>().Single().Scale = new Vector2(0.5f));
|
||||
AddStep("move mouse to bottom-right", () => InputManager.MoveMouseTo(blueprintContainer.ToScreenSpace(blueprintContainer.LayoutRectangle.BottomRight + new Vector2(10))));
|
||||
|
||||
AddStep("right click", () => InputManager.Click(MouseButton.Right));
|
||||
|
||||
AddUntilStep("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject);
|
||||
AddUntilStep("context menu is visible", () => contextMenuContainer.ChildrenOfType<OsuContextMenu>().Single().State == MenuState.Open);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNudgeSelection()
|
||||
{
|
||||
|
@ -78,6 +78,21 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("Inner container width matches scroll container", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestZoomRangeUpdate()
|
||||
{
|
||||
AddStep("set zoom to 2", () => scrollContainer.Zoom = 2);
|
||||
AddStep("set min zoom to 5", () => scrollContainer.MinZoom = 5);
|
||||
AddAssert("zoom = 5", () => scrollContainer.Zoom == 5);
|
||||
|
||||
AddStep("set max zoom to 10", () => scrollContainer.MaxZoom = 10);
|
||||
AddAssert("zoom = 5", () => scrollContainer.Zoom == 5);
|
||||
|
||||
AddStep("set min zoom to 20", () => scrollContainer.MinZoom = 20);
|
||||
AddStep("set max zoom to 40", () => scrollContainer.MaxZoom = 40);
|
||||
AddAssert("zoom = 20", () => scrollContainer.Zoom == 20);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestZoom0()
|
||||
{
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@ -13,6 +14,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Overlays.Toolbar;
|
||||
using osu.Game.Rulesets;
|
||||
using osuTK.Graphics;
|
||||
@ -28,21 +30,44 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
[Resolved]
|
||||
private IRulesetStore rulesets { get; set; }
|
||||
|
||||
private readonly Mock<INotificationOverlay> notifications = new Mock<INotificationOverlay>();
|
||||
[Cached]
|
||||
private readonly NowPlayingOverlay nowPlayingOverlay = new NowPlayingOverlay
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Y = Toolbar.HEIGHT,
|
||||
};
|
||||
|
||||
[Cached]
|
||||
private readonly VolumeOverlay volumeOverlay = new VolumeOverlay
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
};
|
||||
|
||||
private readonly Mock<TestNotificationOverlay> notifications = new Mock<TestNotificationOverlay>();
|
||||
|
||||
private readonly BindableInt unreadNotificationCount = new BindableInt();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Dependencies.CacheAs(notifications.Object);
|
||||
Dependencies.CacheAs<INotificationOverlay>(notifications.Object);
|
||||
notifications.SetupGet(n => n.UnreadCount).Returns(unreadNotificationCount);
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
Child = toolbar = new TestToolbar { State = { Value = Visibility.Visible } };
|
||||
Remove(nowPlayingOverlay);
|
||||
Remove(volumeOverlay);
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
nowPlayingOverlay,
|
||||
volumeOverlay,
|
||||
toolbar = new TestToolbar { State = { Value = Visibility.Visible } },
|
||||
};
|
||||
});
|
||||
|
||||
[Test]
|
||||
@ -122,9 +147,51 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
AddAssert("not scrolled", () => scroll.Current == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVolumeControlViaMusicButtonScroll()
|
||||
{
|
||||
AddStep("hover toolbar music button", () => InputManager.MoveMouseTo(this.ChildrenOfType<ToolbarMusicButton>().Single()));
|
||||
|
||||
AddStep("reset volume", () => Audio.Volume.Value = 1);
|
||||
|
||||
AddRepeatStep("scroll down", () => InputManager.ScrollVerticalBy(-10), 5);
|
||||
AddAssert("volume lowered down", () => Audio.Volume.Value < 1);
|
||||
AddRepeatStep("scroll up", () => InputManager.ScrollVerticalBy(10), 5);
|
||||
AddAssert("volume raised up", () => Audio.Volume.Value == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVolumeControlViaMusicButtonArrowKeys()
|
||||
{
|
||||
AddStep("hover toolbar music button", () => InputManager.MoveMouseTo(this.ChildrenOfType<ToolbarMusicButton>().Single()));
|
||||
|
||||
AddStep("reset volume", () => Audio.Volume.Value = 1);
|
||||
|
||||
AddRepeatStep("arrow down", () => InputManager.Key(Key.Down), 5);
|
||||
AddAssert("volume lowered down", () => Audio.Volume.Value < 1);
|
||||
AddRepeatStep("arrow up", () => InputManager.Key(Key.Up), 5);
|
||||
AddAssert("volume raised up", () => Audio.Volume.Value == 1);
|
||||
}
|
||||
|
||||
public class TestToolbar : Toolbar
|
||||
{
|
||||
public new Bindable<OverlayActivation> OverlayActivationMode => base.OverlayActivationMode as Bindable<OverlayActivation>;
|
||||
}
|
||||
|
||||
// interface mocks break hot reload, mocking this stub implementation instead works around it.
|
||||
// see: https://github.com/moq/moq4/issues/1252
|
||||
[UsedImplicitly]
|
||||
public class TestNotificationOverlay : INotificationOverlay
|
||||
{
|
||||
public virtual void Post(Notification notification)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void Hide()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual IBindable<int> UnreadCount => null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Tournament.Components
|
||||
{
|
||||
if (team == null) return;
|
||||
|
||||
Size = new Vector2(75, 50);
|
||||
Size = new Vector2(75, 54);
|
||||
Masking = true;
|
||||
CornerRadius = 5;
|
||||
Child = flagSprite = new Sprite
|
||||
|
@ -310,7 +310,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
|
||||
public class ScrollingTeam : DrawableTournamentTeam
|
||||
{
|
||||
public const float WIDTH = 58;
|
||||
public const float HEIGHT = 41;
|
||||
public const float HEIGHT = 44;
|
||||
|
||||
private readonly Box outline;
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
// 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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@ -13,7 +15,6 @@ using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
@ -25,15 +26,13 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Skinning;
|
||||
using Realms;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Stores
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
|
||||
/// </summary>
|
||||
[ExcludeFromDynamicCompile]
|
||||
public abstract class BeatmapImporter : RealmArchiveModelManager<BeatmapSetInfo>, IDisposable
|
||||
public class BeatmapImporter : RealmArchiveModelImporter<BeatmapSetInfo>, IDisposable
|
||||
{
|
||||
public override IEnumerable<string> HandledExtensions => new[] { ".osz" };
|
||||
|
||||
@ -41,7 +40,7 @@ namespace osu.Game.Stores
|
||||
|
||||
private readonly BeatmapOnlineLookupQueue? onlineLookupQueue;
|
||||
|
||||
protected BeatmapImporter(RealmAccess realm, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null)
|
||||
public BeatmapImporter(Storage storage, RealmAccess realm, BeatmapOnlineLookupQueue? onlineLookupQueue = null)
|
||||
: base(storage, realm)
|
||||
{
|
||||
this.onlineLookupQueue = onlineLookupQueue;
|
||||
@ -166,11 +165,6 @@ namespace osu.Game.Stores
|
||||
existing.DateAdded = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public override bool IsAvailableLocally(BeatmapSetInfo model)
|
||||
{
|
||||
return Realm.Run(realm => realm.All<BeatmapSetInfo>().Any(s => s.OnlineID == model.OnlineID));
|
||||
}
|
||||
|
||||
public override string HumanisedModelName => "beatmap";
|
||||
|
||||
protected override BeatmapSetInfo? CreateModel(ArchiveReader reader)
|
@ -1,19 +1,26 @@
|
||||
// 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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Online.API;
|
||||
@ -21,32 +28,27 @@ using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Stores;
|
||||
using osu.Game.Utils;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles general operations related to global beatmap management.
|
||||
/// </summary>
|
||||
[ExcludeFromDynamicCompile]
|
||||
public class BeatmapManager : IModelManager<BeatmapSetInfo>, IModelFileManager<BeatmapSetInfo, RealmNamedFileUsage>, IModelImporter<BeatmapSetInfo>, IWorkingBeatmapCache, IDisposable
|
||||
public class BeatmapManager : ModelManager<BeatmapSetInfo>, IModelImporter<BeatmapSetInfo>, IWorkingBeatmapCache, IDisposable
|
||||
{
|
||||
public ITrackStore BeatmapTrackStore { get; }
|
||||
|
||||
private readonly BeatmapModelManager beatmapModelManager;
|
||||
private readonly BeatmapImporter beatmapImporter;
|
||||
|
||||
private readonly WorkingBeatmapCache workingBeatmapCache;
|
||||
private readonly BeatmapOnlineLookupQueue? onlineBeatmapLookupQueue;
|
||||
|
||||
private readonly RealmAccess realm;
|
||||
|
||||
public BeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider? api, AudioManager audioManager, IResourceStore<byte[]> gameResources, GameHost? host = null, WorkingBeatmap? defaultBeatmap = null, bool performOnlineLookups = false)
|
||||
public BeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider? api, AudioManager audioManager, IResourceStore<byte[]> gameResources, GameHost? host = null,
|
||||
WorkingBeatmap? defaultBeatmap = null, bool performOnlineLookups = false)
|
||||
: base(storage, realm)
|
||||
{
|
||||
this.realm = realm;
|
||||
|
||||
if (performOnlineLookups)
|
||||
{
|
||||
if (api == null)
|
||||
@ -59,19 +61,20 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
BeatmapTrackStore = audioManager.GetTrackStore(userResources);
|
||||
|
||||
beatmapModelManager = CreateBeatmapModelManager(storage, realm, rulesets, onlineBeatmapLookupQueue);
|
||||
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
|
||||
beatmapImporter = CreateBeatmapImporter(storage, realm, rulesets, onlineBeatmapLookupQueue);
|
||||
beatmapImporter.PostNotification = obj => PostNotification?.Invoke(obj);
|
||||
|
||||
beatmapModelManager.WorkingBeatmapCache = workingBeatmapCache;
|
||||
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
|
||||
}
|
||||
|
||||
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap? defaultBeatmap, GameHost? host)
|
||||
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap? defaultBeatmap,
|
||||
GameHost? host)
|
||||
{
|
||||
return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host);
|
||||
}
|
||||
|
||||
protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue? onlineLookupQueue) =>
|
||||
new BeatmapModelManager(realm, storage, onlineLookupQueue);
|
||||
protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue? onlineLookupQueue) =>
|
||||
new BeatmapImporter(storage, realm, onlineLookupQueue);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new beatmap set, backed by a <see cref="BeatmapSetInfo"/> model,
|
||||
@ -100,7 +103,7 @@ namespace osu.Game.Beatmaps
|
||||
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
|
||||
b.BeatmapSet = beatmapSet;
|
||||
|
||||
var imported = beatmapModelManager.Import(beatmapSet);
|
||||
var imported = beatmapImporter.Import(beatmapSet);
|
||||
|
||||
if (imported == null)
|
||||
throw new InvalidOperationException("Failed to import new beatmap");
|
||||
@ -170,12 +173,12 @@ namespace osu.Game.Beatmaps
|
||||
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()`
|
||||
// several places like `Save()` or `GetWorkingBeatmap()`
|
||||
// rely on them being freely traversable in both directions for correct operation.
|
||||
targetBeatmapSet.Beatmaps.Add(newBeatmap.BeatmapInfo);
|
||||
newBeatmap.BeatmapInfo.BeatmapSet = targetBeatmapSet;
|
||||
|
||||
beatmapModelManager.Save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin);
|
||||
Save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin);
|
||||
|
||||
workingBeatmapCache.Invalidate(targetBeatmapSet);
|
||||
return GetWorkingBeatmap(newBeatmap.BeatmapInfo);
|
||||
@ -187,7 +190,7 @@ namespace osu.Game.Beatmaps
|
||||
/// <param name="beatmapInfo">The beatmap difficulty to hide.</param>
|
||||
public void Hide(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
realm.Run(r =>
|
||||
Realm.Run(r =>
|
||||
{
|
||||
using (var transaction = r.BeginWrite())
|
||||
{
|
||||
@ -206,7 +209,7 @@ namespace osu.Game.Beatmaps
|
||||
/// <param name="beatmapInfo">The beatmap difficulty to restore.</param>
|
||||
public void Restore(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
realm.Run(r =>
|
||||
Realm.Run(r =>
|
||||
{
|
||||
using (var transaction = r.BeginWrite())
|
||||
{
|
||||
@ -221,7 +224,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public void RestoreAll()
|
||||
{
|
||||
realm.Run(r =>
|
||||
Realm.Run(r =>
|
||||
{
|
||||
using (var transaction = r.BeginWrite())
|
||||
{
|
||||
@ -239,7 +242,7 @@ namespace osu.Game.Beatmaps
|
||||
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
|
||||
public List<BeatmapSetInfo> GetAllUsableBeatmapSets()
|
||||
{
|
||||
return realm.Run(r =>
|
||||
return Realm.Run(r =>
|
||||
{
|
||||
r.Refresh();
|
||||
return r.All<BeatmapSetInfo>().Where(b => !b.DeletePending).Detach();
|
||||
@ -253,26 +256,15 @@ namespace osu.Game.Beatmaps
|
||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||
public Live<BeatmapSetInfo>? QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query)
|
||||
{
|
||||
return realm.Run(r => r.All<BeatmapSetInfo>().FirstOrDefault(query)?.ToLive(realm));
|
||||
return Realm.Run(r => r.All<BeatmapSetInfo>().FirstOrDefault(query)?.ToLive(Realm));
|
||||
}
|
||||
|
||||
#region Delegation to BeatmapModelManager (methods which previously existed locally).
|
||||
|
||||
/// <summary>
|
||||
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => beatmapModelManager.QueryBeatmap(query)?.Detach();
|
||||
|
||||
/// <summary>
|
||||
/// Saves an <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param>
|
||||
/// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param>
|
||||
/// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
|
||||
public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin? beatmapSkin = null) =>
|
||||
beatmapModelManager.Save(info, beatmapContent, beatmapSkin);
|
||||
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => Realm.Run(r => r.All<BeatmapInfo>().FirstOrDefault(query)?.Detach());
|
||||
|
||||
/// <summary>
|
||||
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
|
||||
@ -280,86 +272,152 @@ namespace osu.Game.Beatmaps
|
||||
public IWorkingBeatmap DefaultBeatmap => workingBeatmapCache.DefaultBeatmap;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a notification should be presented to the user.
|
||||
/// Saves an <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
|
||||
/// </summary>
|
||||
public Action<Notification> PostNotification
|
||||
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param>
|
||||
/// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param>
|
||||
/// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
|
||||
public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null)
|
||||
{
|
||||
set => beatmapModelManager.PostNotification = value;
|
||||
var setInfo = beatmapInfo.BeatmapSet;
|
||||
Debug.Assert(setInfo != null);
|
||||
|
||||
// Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
|
||||
// This should hopefully be temporary, assuming said clone is eventually removed.
|
||||
|
||||
// Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved)
|
||||
// *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation).
|
||||
// CopyTo() will undo such adjustments, while CopyFrom() will not.
|
||||
beatmapContent.Difficulty.CopyTo(beatmapInfo.Difficulty);
|
||||
|
||||
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
|
||||
beatmapContent.BeatmapInfo = beatmapInfo;
|
||||
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
|
||||
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
// AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
|
||||
var existingFileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase));
|
||||
string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo);
|
||||
|
||||
// ensure that two difficulties from the set don't point at the same beatmap file.
|
||||
if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase)))
|
||||
throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'.");
|
||||
|
||||
if (existingFileInfo != null)
|
||||
DeleteFile(setInfo, existingFileInfo);
|
||||
|
||||
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
|
||||
beatmapInfo.Hash = stream.ComputeSHA2Hash();
|
||||
|
||||
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
|
||||
|
||||
Realm.Write(r => setInfo.CopyChangesToRealm(r.Find<BeatmapSetInfo>(setInfo.ID)));
|
||||
}
|
||||
|
||||
workingBeatmapCache.Invalidate(beatmapInfo);
|
||||
|
||||
static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
var metadata = beatmapInfo.Metadata;
|
||||
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IModelManager<BeatmapSetInfo>
|
||||
|
||||
public bool IsAvailableLocally(BeatmapSetInfo model)
|
||||
public void DeleteAllVideos()
|
||||
{
|
||||
return beatmapModelManager.IsAvailableLocally(model);
|
||||
}
|
||||
|
||||
public bool Delete(BeatmapSetInfo item)
|
||||
{
|
||||
return beatmapModelManager.Delete(item);
|
||||
}
|
||||
|
||||
public void Delete(List<BeatmapSetInfo> items, bool silent = false)
|
||||
{
|
||||
beatmapModelManager.Delete(items, silent);
|
||||
Realm.Write(r =>
|
||||
{
|
||||
var items = r.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected);
|
||||
DeleteVideos(items.ToList());
|
||||
});
|
||||
}
|
||||
|
||||
public void Delete(Expression<Func<BeatmapSetInfo, bool>>? filter = null, bool silent = false)
|
||||
{
|
||||
realm.Run(r =>
|
||||
Realm.Run(r =>
|
||||
{
|
||||
var items = r.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected);
|
||||
|
||||
if (filter != null)
|
||||
items = items.Where(filter);
|
||||
|
||||
beatmapModelManager.Delete(items.ToList(), silent);
|
||||
Delete(items.ToList(), silent);
|
||||
});
|
||||
}
|
||||
|
||||
public void DeleteAllVideos()
|
||||
/// <summary>
|
||||
/// Delete videos from a list of beatmaps.
|
||||
/// This will post notifications tracking progress.
|
||||
/// </summary>
|
||||
public void DeleteVideos(List<BeatmapSetInfo> items, bool silent = false)
|
||||
{
|
||||
realm.Write(r =>
|
||||
if (items.Count == 0) return;
|
||||
|
||||
var notification = new ProgressNotification
|
||||
{
|
||||
var items = r.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected);
|
||||
beatmapModelManager.DeleteVideos(items.ToList());
|
||||
});
|
||||
Progress = 0,
|
||||
Text = $"Preparing to delete all {HumanisedModelName} videos...",
|
||||
CompletionText = "No videos found to delete!",
|
||||
State = ProgressNotificationState.Active,
|
||||
};
|
||||
|
||||
if (!silent)
|
||||
PostNotification?.Invoke(notification);
|
||||
|
||||
int i = 0;
|
||||
int deleted = 0;
|
||||
|
||||
foreach (var b in items)
|
||||
{
|
||||
if (notification.State == ProgressNotificationState.Cancelled)
|
||||
// user requested abort
|
||||
return;
|
||||
|
||||
var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.Ordinal)));
|
||||
|
||||
if (video != null)
|
||||
{
|
||||
DeleteFile(b, video);
|
||||
deleted++;
|
||||
notification.CompletionText = $"Deleted {deleted} {HumanisedModelName} video(s)!";
|
||||
}
|
||||
|
||||
notification.Text = $"Deleting videos from {HumanisedModelName}s ({deleted} deleted)";
|
||||
|
||||
notification.Progress = (float)++i / items.Count;
|
||||
}
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
}
|
||||
|
||||
public void UndeleteAll()
|
||||
{
|
||||
realm.Run(r => beatmapModelManager.Undelete(r.All<BeatmapSetInfo>().Where(s => s.DeletePending).ToList()));
|
||||
Realm.Run(r => Undelete(r.All<BeatmapSetInfo>().Where(s => s.DeletePending).ToList()));
|
||||
}
|
||||
|
||||
public void Undelete(List<BeatmapSetInfo> items, bool silent = false)
|
||||
{
|
||||
beatmapModelManager.Undelete(items, silent);
|
||||
}
|
||||
|
||||
public void Undelete(BeatmapSetInfo item)
|
||||
{
|
||||
beatmapModelManager.Undelete(item);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of ICanAcceptFiles
|
||||
|
||||
public Task Import(params string[] paths) => beatmapModelManager.Import(paths);
|
||||
public Task Import(params string[] paths) => beatmapImporter.Import(paths);
|
||||
|
||||
public Task Import(params ImportTask[] tasks) => beatmapModelManager.Import(tasks);
|
||||
public Task Import(params ImportTask[] tasks) => beatmapImporter.Import(tasks);
|
||||
|
||||
public Task<IEnumerable<Live<BeatmapSetInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => beatmapModelManager.Import(notification, tasks);
|
||||
public Task<IEnumerable<Live<BeatmapSetInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => beatmapImporter.Import(notification, tasks);
|
||||
|
||||
public Task<Live<BeatmapSetInfo>?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) => beatmapModelManager.Import(task, lowPriority, cancellationToken);
|
||||
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 lowPriority = false, CancellationToken cancellationToken = default) => beatmapModelManager.Import(archive, lowPriority, 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) => beatmapModelManager.Import(item, archive, cancellationToken);
|
||||
public Live<BeatmapSetInfo>? Import(BeatmapSetInfo item, ArchiveReader? archive = null, CancellationToken cancellationToken = default) =>
|
||||
beatmapImporter.Import(item, archive, false, cancellationToken);
|
||||
|
||||
public IEnumerable<string> HandledExtensions => beatmapModelManager.HandledExtensions;
|
||||
public IEnumerable<string> HandledExtensions => beatmapImporter.HandledExtensions;
|
||||
|
||||
#endregion
|
||||
|
||||
@ -371,7 +429,7 @@ namespace osu.Game.Beatmaps
|
||||
// If we seem to be missing files, now is a good time to re-fetch.
|
||||
if (importedBeatmap?.BeatmapSet?.Files.Count == 0)
|
||||
{
|
||||
realm.Run(r =>
|
||||
Realm.Run(r =>
|
||||
{
|
||||
var refetch = r.Find<BeatmapInfo>(importedBeatmap.ID)?.Detach();
|
||||
|
||||
@ -383,36 +441,10 @@ namespace osu.Game.Beatmaps
|
||||
return workingBeatmapCache.GetWorkingBeatmap(importedBeatmap);
|
||||
}
|
||||
|
||||
public WorkingBeatmap GetWorkingBeatmap(Live<BeatmapInfo>? importedBeatmap)
|
||||
{
|
||||
WorkingBeatmap working = workingBeatmapCache.GetWorkingBeatmap(null);
|
||||
|
||||
importedBeatmap?.PerformRead(b => working = workingBeatmapCache.GetWorkingBeatmap(b));
|
||||
|
||||
return working;
|
||||
}
|
||||
|
||||
void IWorkingBeatmapCache.Invalidate(BeatmapSetInfo beatmapSetInfo) => workingBeatmapCache.Invalidate(beatmapSetInfo);
|
||||
void IWorkingBeatmapCache.Invalidate(BeatmapInfo beatmapInfo) => workingBeatmapCache.Invalidate(beatmapInfo);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IModelFileManager<in BeatmapSetInfo,in BeatmapSetFileInfo>
|
||||
|
||||
public void ReplaceFile(BeatmapSetInfo model, RealmNamedFileUsage file, Stream contents)
|
||||
{
|
||||
beatmapModelManager.ReplaceFile(model, file, contents);
|
||||
}
|
||||
|
||||
public void DeleteFile(BeatmapSetInfo model, RealmNamedFileUsage file)
|
||||
{
|
||||
beatmapModelManager.DeleteFile(model, file);
|
||||
}
|
||||
|
||||
public void AddFile(BeatmapSetInfo model, Stream contents, string filename)
|
||||
{
|
||||
beatmapModelManager.AddFile(model, contents, filename);
|
||||
}
|
||||
public override bool IsAvailableLocally(BeatmapSetInfo model) => Realm.Run(realm => realm.All<BeatmapSetInfo>().Any(s => s.OnlineID == model.OnlineID));
|
||||
|
||||
#endregion
|
||||
|
||||
@ -429,9 +461,11 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public Action<IEnumerable<Live<BeatmapSetInfo>>>? PostImport
|
||||
{
|
||||
set => beatmapModelManager.PostImport = value;
|
||||
set => beatmapImporter.PostImport = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override string HumanisedModelName => "beatmap";
|
||||
}
|
||||
}
|
||||
|
@ -1,166 +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;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Stores;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
[ExcludeFromDynamicCompile]
|
||||
public class BeatmapModelManager : BeatmapImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// The game working beatmap cache, used to invalidate entries on changes.
|
||||
/// </summary>
|
||||
public IWorkingBeatmapCache? WorkingBeatmapCache { private get; set; }
|
||||
|
||||
public override IEnumerable<string> HandledExtensions => new[] { ".osz" };
|
||||
|
||||
protected override string[] HashableFileTypes => new[] { ".osu" };
|
||||
|
||||
public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv" };
|
||||
|
||||
public BeatmapModelManager(RealmAccess realm, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null)
|
||||
: base(realm, storage, onlineLookupQueue)
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz";
|
||||
|
||||
/// <summary>
|
||||
/// Saves an <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
|
||||
/// </summary>
|
||||
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param>
|
||||
/// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param>
|
||||
/// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
|
||||
public void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null)
|
||||
{
|
||||
var setInfo = beatmapInfo.BeatmapSet;
|
||||
Debug.Assert(setInfo != null);
|
||||
|
||||
// Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
|
||||
// This should hopefully be temporary, assuming said clone is eventually removed.
|
||||
|
||||
// Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved)
|
||||
// *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation).
|
||||
// CopyTo() will undo such adjustments, while CopyFrom() will not.
|
||||
beatmapContent.Difficulty.CopyTo(beatmapInfo.Difficulty);
|
||||
|
||||
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
|
||||
beatmapContent.BeatmapInfo = beatmapInfo;
|
||||
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
|
||||
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
// AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
|
||||
var existingFileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase));
|
||||
string targetFilename = getFilename(beatmapInfo);
|
||||
|
||||
// ensure that two difficulties from the set don't point at the same beatmap file.
|
||||
if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase)))
|
||||
throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'.");
|
||||
|
||||
if (existingFileInfo != null)
|
||||
DeleteFile(setInfo, existingFileInfo);
|
||||
|
||||
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
|
||||
beatmapInfo.Hash = stream.ComputeSHA2Hash();
|
||||
|
||||
AddFile(setInfo, stream, getFilename(beatmapInfo));
|
||||
Update(setInfo);
|
||||
}
|
||||
|
||||
WorkingBeatmapCache?.Invalidate(beatmapInfo);
|
||||
}
|
||||
|
||||
private static string getFilename(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
var metadata = beatmapInfo.Metadata;
|
||||
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query)
|
||||
{
|
||||
return Realm.Run(realm => realm.All<BeatmapInfo>().FirstOrDefault(query)?.Detach());
|
||||
}
|
||||
|
||||
public void Update(BeatmapSetInfo item)
|
||||
{
|
||||
Realm.Write(r =>
|
||||
{
|
||||
var existing = r.Find<BeatmapSetInfo>(item.ID);
|
||||
item.CopyChangesToRealm(existing);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete videos from a list of beatmaps.
|
||||
/// This will post notifications tracking progress.
|
||||
/// </summary>
|
||||
public void DeleteVideos(List<BeatmapSetInfo> items, bool silent = false)
|
||||
{
|
||||
if (items.Count == 0) return;
|
||||
|
||||
var notification = new ProgressNotification
|
||||
{
|
||||
Progress = 0,
|
||||
Text = $"Preparing to delete all {HumanisedModelName} videos...",
|
||||
CompletionText = "No videos found to delete!",
|
||||
State = ProgressNotificationState.Active,
|
||||
};
|
||||
|
||||
if (!silent)
|
||||
PostNotification?.Invoke(notification);
|
||||
|
||||
int i = 0;
|
||||
int deleted = 0;
|
||||
|
||||
foreach (var b in items)
|
||||
{
|
||||
if (notification.State == ProgressNotificationState.Cancelled)
|
||||
// user requested abort
|
||||
return;
|
||||
|
||||
var video = b.Files.FirstOrDefault(f => VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.Ordinal)));
|
||||
|
||||
if (video != null)
|
||||
{
|
||||
DeleteFile(b, video);
|
||||
deleted++;
|
||||
notification.CompletionText = $"Deleted {deleted} {HumanisedModelName} video(s)!";
|
||||
}
|
||||
|
||||
notification.Text = $"Deleting videos from {HumanisedModelName}s ({deleted} deleted)";
|
||||
|
||||
notification.Progress = (float)++i / items.Count;
|
||||
}
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,6 @@ using osu.Framework.Threading;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Stores;
|
||||
using SharpCompress.Compressors;
|
||||
using SharpCompress.Compressors.BZip2;
|
||||
|
||||
|
@ -6,14 +6,13 @@
|
||||
using System.IO;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Stores;
|
||||
using osu.Game.Utils;
|
||||
using SharpCompress.Common;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// An encapsulated import task to be imported to an <see cref="RealmArchiveModelManager{TModel}"/>.
|
||||
/// An encapsulated import task to be imported to an <see cref="RealmArchiveModelImporter{TModel}"/>.
|
||||
/// </summary>
|
||||
public class ImportTask
|
||||
{
|
||||
|
@ -1,6 +1,9 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.IO;
|
||||
@ -13,6 +16,24 @@ namespace osu.Game.Database
|
||||
|
||||
protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage();
|
||||
|
||||
protected override IEnumerable<string> GetStableImportPaths(Storage storage)
|
||||
{
|
||||
foreach (string directory in storage.GetDirectories(string.Empty))
|
||||
{
|
||||
var directoryStorage = storage.GetStorageForDirectory(directory);
|
||||
|
||||
if (!directoryStorage.GetFiles(string.Empty).ExcludeSystemFileNames().Any())
|
||||
{
|
||||
// if a directory doesn't contain files, attempt looking for beatmaps inside of that directory.
|
||||
// this is a special behaviour in stable for beatmaps only, see https://github.com/ppy/osu/issues/18615.
|
||||
foreach (string subDirectory in GetStableImportPaths(directoryStorage))
|
||||
yield return subDirectory;
|
||||
}
|
||||
else
|
||||
yield return storage.GetFullPath(directory);
|
||||
}
|
||||
}
|
||||
|
||||
public LegacyBeatmapImporter(IModelImporter<BeatmapSetInfo> importer)
|
||||
: base(importer)
|
||||
{
|
||||
|
@ -1,33 +1,31 @@
|
||||
// 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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using Realms;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Stores
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// Class which adds all the missing pieces bridging the gap between <see cref="RealmArchiveModelImporter{TModel}"/> and (legacy) ArchiveModelManager.
|
||||
/// </summary>
|
||||
public abstract class RealmArchiveModelManager<TModel> : RealmArchiveModelImporter<TModel>, IModelManager<TModel>, IModelFileManager<TModel, RealmNamedFileUsage>
|
||||
public class ModelManager<TModel> : IModelManager<TModel>, IModelFileManager<TModel, RealmNamedFileUsage>
|
||||
where TModel : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey, ISoftDelete
|
||||
{
|
||||
protected RealmAccess Realm { get; }
|
||||
|
||||
private readonly RealmFileStore realmFileStore;
|
||||
|
||||
protected RealmArchiveModelManager(Storage storage, RealmAccess realm)
|
||||
: base(storage, realm)
|
||||
public ModelManager(Storage storage, RealmAccess realm)
|
||||
{
|
||||
realmFileStore = new RealmFileStore(realm, storage);
|
||||
Realm = realm;
|
||||
}
|
||||
|
||||
public void DeleteFile(TModel item, RealmNamedFileUsage file) =>
|
||||
@ -63,7 +61,7 @@ namespace osu.Game.Stores
|
||||
/// <summary>
|
||||
/// Delete a file from within an ongoing realm transaction.
|
||||
/// </summary>
|
||||
protected void DeleteFile(TModel item, RealmNamedFileUsage file, Realm realm)
|
||||
public void DeleteFile(TModel item, RealmNamedFileUsage file, Realm realm)
|
||||
{
|
||||
item.Files.Remove(file);
|
||||
}
|
||||
@ -71,7 +69,7 @@ namespace osu.Game.Stores
|
||||
/// <summary>
|
||||
/// Replace a file from within an ongoing realm transaction.
|
||||
/// </summary>
|
||||
protected void ReplaceFile(RealmNamedFileUsage file, Stream contents, Realm realm)
|
||||
public void ReplaceFile(RealmNamedFileUsage file, Stream contents, Realm realm)
|
||||
{
|
||||
file.File = realmFileStore.Add(contents, realm);
|
||||
}
|
||||
@ -79,7 +77,7 @@ namespace osu.Game.Stores
|
||||
/// <summary>
|
||||
/// Add a file from within an ongoing realm transaction. If the file already exists, it is overwritten.
|
||||
/// </summary>
|
||||
protected void AddFile(TModel item, Stream contents, string filename, Realm realm)
|
||||
public void AddFile(TModel item, Stream contents, string filename, Realm realm)
|
||||
{
|
||||
var existing = item.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
@ -201,6 +199,10 @@ namespace osu.Game.Stores
|
||||
});
|
||||
}
|
||||
|
||||
public abstract bool IsAvailableLocally(TModel model);
|
||||
public virtual bool IsAvailableLocally(TModel model) => true;
|
||||
|
||||
public Action<Notification>? PostNotification { get; set; }
|
||||
|
||||
public virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}";
|
||||
}
|
||||
}
|
@ -28,7 +28,6 @@ using osu.Game.Models;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Stores;
|
||||
using Realms;
|
||||
using Realms.Exceptions;
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
// 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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@ -13,16 +15,13 @@ using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using Realms;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Stores
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// Encapsulates a model store class to give it import functionality.
|
||||
@ -32,12 +31,16 @@ namespace osu.Game.Stores
|
||||
public abstract class RealmArchiveModelImporter<TModel> : IModelImporter<TModel>
|
||||
where TModel : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey, ISoftDelete
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum number of concurrent imports to run per import scheduler.
|
||||
/// </summary>
|
||||
private const int import_queue_request_concurrency = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The size of a batch import operation before considering it a lower priority operation.
|
||||
/// The minimum number of items in a single import call in order for the import to be processed as a batch.
|
||||
/// Batch imports will apply optimisations preferring speed over consistency when detecting changes in already-imported items.
|
||||
/// </summary>
|
||||
private const int low_priority_import_batch_size = 1;
|
||||
private const int minimum_items_considered_batch_import = 10;
|
||||
|
||||
/// <summary>
|
||||
/// A singleton scheduler shared by all <see cref="RealmArchiveModelImporter{TModel}"/>.
|
||||
@ -49,11 +52,11 @@ namespace osu.Game.Stores
|
||||
private static readonly ThreadedTaskScheduler import_scheduler = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(RealmArchiveModelImporter<TModel>));
|
||||
|
||||
/// <summary>
|
||||
/// A second scheduler for lower priority imports.
|
||||
/// A second scheduler for batch imports.
|
||||
/// For simplicity, these will just run in parallel with normal priority imports, but a future refactor would see this implemented via a custom scheduler/queue.
|
||||
/// See https://gist.github.com/peppy/f0e118a14751fc832ca30dd48ba3876b for an incomplete version of this.
|
||||
/// </summary>
|
||||
private static readonly ThreadedTaskScheduler import_scheduler_low_priority = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(RealmArchiveModelImporter<TModel>));
|
||||
private static readonly ThreadedTaskScheduler import_scheduler_batch = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(RealmArchiveModelImporter<TModel>));
|
||||
|
||||
public virtual IEnumerable<string> HandledExtensions => new[] { @".zip" };
|
||||
|
||||
@ -69,7 +72,7 @@ namespace osu.Game.Stores
|
||||
/// <summary>
|
||||
/// Set an endpoint for notifications to be posted to.
|
||||
/// </summary>
|
||||
public Action<Notification>? PostNotification { protected get; set; }
|
||||
public Action<Notification>? PostNotification { get; set; }
|
||||
|
||||
protected RealmArchiveModelImporter(Storage storage, RealmAccess realm)
|
||||
{
|
||||
@ -105,7 +108,7 @@ namespace osu.Game.Stores
|
||||
|
||||
var imported = new List<Live<TModel>>();
|
||||
|
||||
bool isLowPriorityImport = tasks.Length > low_priority_import_batch_size;
|
||||
bool isBatchImport = tasks.Length >= minimum_items_considered_batch_import;
|
||||
|
||||
try
|
||||
{
|
||||
@ -115,7 +118,7 @@ namespace osu.Game.Stores
|
||||
|
||||
try
|
||||
{
|
||||
var model = await Import(task, isLowPriorityImport, notification.CancellationToken).ConfigureAwait(false);
|
||||
var model = await Import(task, isBatchImport, notification.CancellationToken).ConfigureAwait(false);
|
||||
|
||||
lock (imported)
|
||||
{
|
||||
@ -178,16 +181,16 @@ namespace osu.Game.Stores
|
||||
/// Note that this bypasses the UI flow and should only be used for special cases or testing.
|
||||
/// </summary>
|
||||
/// <param name="task">The <see cref="ImportTask"/> containing data about the <typeparamref name="TModel"/> to import.</param>
|
||||
/// <param name="lowPriority">Whether this is a low priority import.</param>
|
||||
/// <param name="batchImport">Whether this import is part of a larger batch.</param>
|
||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||
/// <returns>The imported model, if successful.</returns>
|
||||
public async Task<Live<TModel>?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default)
|
||||
public async Task<Live<TModel>?> Import(ImportTask task, bool batchImport = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
Live<TModel>? import;
|
||||
using (ArchiveReader reader = task.GetReader())
|
||||
import = await Import(reader, lowPriority, cancellationToken).ConfigureAwait(false);
|
||||
import = await Import(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.
|
||||
@ -210,9 +213,9 @@ namespace osu.Game.Stores
|
||||
/// Silently import an item from an <see cref="ArchiveReader"/>.
|
||||
/// </summary>
|
||||
/// <param name="archive">The archive to be imported.</param>
|
||||
/// <param name="lowPriority">Whether this is a low priority import.</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 lowPriority = false, CancellationToken cancellationToken = default)
|
||||
public async Task<Live<TModel>?> Import(ArchiveReader archive, bool batchImport = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@ -235,10 +238,10 @@ namespace osu.Game.Stores
|
||||
return null;
|
||||
}
|
||||
|
||||
var scheduledImport = Task.Factory.StartNew(() => Import(model, archive, cancellationToken),
|
||||
var scheduledImport = Task.Factory.StartNew(() => Import(model, archive, batchImport, cancellationToken),
|
||||
cancellationToken,
|
||||
TaskCreationOptions.HideScheduler,
|
||||
lowPriority ? import_scheduler_low_priority : import_scheduler);
|
||||
batchImport ? import_scheduler_batch : import_scheduler);
|
||||
|
||||
return await scheduledImport.ConfigureAwait(false);
|
||||
}
|
||||
@ -248,106 +251,104 @@ namespace osu.Game.Stores
|
||||
/// </summary>
|
||||
/// <param name="item">The model to be imported.</param>
|
||||
/// <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, CancellationToken cancellationToken = default)
|
||||
public virtual Live<TModel>? Import(TModel item, ArchiveReader? archive = null, bool batchImport = false, CancellationToken cancellationToken = default) => Realm.Run(realm =>
|
||||
{
|
||||
return Realm.Run(realm =>
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
bool checkedExisting = false;
|
||||
TModel? existing = null;
|
||||
|
||||
if (batchImport && archive != null)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
// this is a fast bail condition to improve large import performance.
|
||||
item.Hash = computeHashFast(archive);
|
||||
|
||||
bool checkedExisting = false;
|
||||
TModel? existing = null;
|
||||
checkedExisting = true;
|
||||
existing = CheckForExisting(item, realm);
|
||||
|
||||
if (archive != null && !HasCustomHashFunction)
|
||||
if (existing != null)
|
||||
{
|
||||
// this is a fast bail condition to improve large import performance.
|
||||
item.Hash = computeHashFast(archive);
|
||||
// bare minimum comparisons
|
||||
//
|
||||
// note that this should really be checking filesizes on disk (of existing files) for some degree of sanity.
|
||||
// or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files.
|
||||
if (CanSkipImport(existing, item) &&
|
||||
getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f)) &&
|
||||
checkAllFilesExist(existing))
|
||||
{
|
||||
LogForModel(item, @$"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import.");
|
||||
|
||||
checkedExisting = true;
|
||||
existing = CheckForExisting(item, realm);
|
||||
using (var transaction = realm.BeginWrite())
|
||||
{
|
||||
UndeleteForReuse(existing);
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
return existing.ToLive(Realm);
|
||||
}
|
||||
|
||||
LogForModel(item, @"Found existing (optimised) but failed pre-check.");
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LogForModel(item, @"Beginning import...");
|
||||
|
||||
// TODO: do we want to make the transaction this local? not 100% sure, will need further investigation.
|
||||
using (var transaction = realm.BeginWrite())
|
||||
{
|
||||
if (archive != null)
|
||||
// TODO: look into rollback of file additions (or delayed commit).
|
||||
item.Files.AddRange(createFileInfos(archive, Files, realm));
|
||||
|
||||
item.Hash = ComputeHash(item);
|
||||
|
||||
// TODO: we may want to run this outside of the transaction.
|
||||
Populate(item, archive, realm, cancellationToken);
|
||||
|
||||
if (!checkedExisting)
|
||||
existing = CheckForExisting(item, realm);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
// bare minimum comparisons
|
||||
//
|
||||
// note that this should really be checking filesizes on disk (of existing files) for some degree of sanity.
|
||||
// or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files.
|
||||
if (CanSkipImport(existing, item) &&
|
||||
getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f)) &&
|
||||
checkAllFilesExist(existing))
|
||||
if (CanReuseExisting(existing, item))
|
||||
{
|
||||
LogForModel(item, @$"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import.");
|
||||
LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import.");
|
||||
|
||||
using (var transaction = realm.BeginWrite())
|
||||
{
|
||||
UndeleteForReuse(existing);
|
||||
transaction.Commit();
|
||||
}
|
||||
UndeleteForReuse(existing);
|
||||
transaction.Commit();
|
||||
|
||||
return existing.ToLive(Realm);
|
||||
}
|
||||
|
||||
LogForModel(item, @"Found existing (optimised) but failed pre-check.");
|
||||
}
|
||||
}
|
||||
LogForModel(item, @"Found existing but failed re-use check.");
|
||||
|
||||
try
|
||||
{
|
||||
LogForModel(item, @"Beginning import...");
|
||||
|
||||
// TODO: do we want to make the transaction this local? not 100% sure, will need further investigation.
|
||||
using (var transaction = realm.BeginWrite())
|
||||
{
|
||||
if (archive != null)
|
||||
// TODO: look into rollback of file additions (or delayed commit).
|
||||
item.Files.AddRange(createFileInfos(archive, Files, realm));
|
||||
|
||||
item.Hash = ComputeHash(item);
|
||||
|
||||
// TODO: we may want to run this outside of the transaction.
|
||||
Populate(item, archive, realm, cancellationToken);
|
||||
|
||||
if (!checkedExisting)
|
||||
existing = CheckForExisting(item, realm);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
if (CanReuseExisting(existing, item))
|
||||
{
|
||||
LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import.");
|
||||
|
||||
UndeleteForReuse(existing);
|
||||
transaction.Commit();
|
||||
|
||||
return existing.ToLive(Realm);
|
||||
}
|
||||
|
||||
LogForModel(item, @"Found existing but failed re-use check.");
|
||||
|
||||
existing.DeletePending = true;
|
||||
}
|
||||
|
||||
PreImport(item, realm);
|
||||
|
||||
// import to store
|
||||
realm.Add(item);
|
||||
|
||||
transaction.Commit();
|
||||
existing.DeletePending = true;
|
||||
}
|
||||
|
||||
LogForModel(item, @"Import successfully completed!");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (!(e is TaskCanceledException))
|
||||
LogForModel(item, @"Database import or population failed and has been rolled back.", e);
|
||||
PreImport(item, realm);
|
||||
|
||||
throw;
|
||||
// import to store
|
||||
realm.Add(item);
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
return (Live<TModel>?)item.ToLive(Realm);
|
||||
});
|
||||
}
|
||||
LogForModel(item, @"Import successfully completed!");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (!(e is TaskCanceledException))
|
||||
LogForModel(item, @"Database import or population failed and has been rolled back.", e);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
return (Live<TModel>?)item.ToLive(Realm);
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Any file extensions which should be included in hash creation.
|
||||
@ -375,19 +376,13 @@ namespace osu.Game.Stores
|
||||
Logger.Log($"{prefix} {message}", LoggingTarget.Database);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the implementation overrides <see cref="ComputeHash"/> with a custom implementation.
|
||||
/// Custom hash implementations must bypass the early exit in the import flow (see <see cref="computeHashFast"/> usage).
|
||||
/// </summary>
|
||||
protected virtual bool HasCustomHashFunction => false;
|
||||
|
||||
/// <summary>
|
||||
/// Create a SHA-2 hash from the provided archive based on file content of all files matching <see cref="HashableFileTypes"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In the case of no matching files, a hash will be generated from the passed archive's <see cref="ArchiveReader.Name"/>.
|
||||
/// </remarks>
|
||||
protected virtual string ComputeHash(TModel item)
|
||||
protected string ComputeHash(TModel item)
|
||||
{
|
||||
// for now, concatenate all hashable files in the set to create a unique hash.
|
||||
MemoryStream hashable = new MemoryStream();
|
@ -1,6 +1,8 @@
|
||||
// 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 System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -9,14 +11,11 @@ using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Models;
|
||||
using Realms;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Stores
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the storing of files to the file system (and database) backing.
|
@ -20,6 +20,12 @@ namespace osu.Game.Graphics.UserInterface
|
||||
TabSelect,
|
||||
|
||||
[Description("scrolltotop")]
|
||||
ScrollToTop
|
||||
ScrollToTop,
|
||||
|
||||
[Description("dialog-cancel")]
|
||||
DialogCancel,
|
||||
|
||||
[Description("dialog-ok")]
|
||||
DialogOk
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -67,7 +66,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BeatmapModelManager.VIDEO_EXTENSIONS.Contains(File.Extension))
|
||||
if (OsuGameBase.VIDEO_EXTENSIONS.Contains(File.Extension))
|
||||
return FontAwesome.Regular.FileVideo;
|
||||
|
||||
switch (File.Extension)
|
||||
|
@ -14,7 +14,7 @@ namespace osu.Game.IO
|
||||
/// </summary>
|
||||
public class StableStorage : DesktopStorage
|
||||
{
|
||||
private const string stable_default_songs_path = "Songs";
|
||||
public const string STABLE_DEFAULT_SONGS_PATH = "Songs";
|
||||
|
||||
private readonly DesktopGameHost host;
|
||||
private readonly Lazy<string> songsPath;
|
||||
@ -62,7 +62,7 @@ namespace osu.Game.IO
|
||||
}
|
||||
}
|
||||
|
||||
return GetFullPath(stable_default_songs_path);
|
||||
return GetFullPath(STABLE_DEFAULT_SONGS_PATH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -183,7 +183,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Size = new Vector2(30f, 20f),
|
||||
Size = new Vector2(28, 20),
|
||||
},
|
||||
new DateLabel(Score.Date)
|
||||
{
|
||||
|
@ -57,6 +57,8 @@ namespace osu.Game
|
||||
/// </summary>
|
||||
public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider
|
||||
{
|
||||
public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv" };
|
||||
|
||||
public const string OSU_PROTOCOL = "osu://";
|
||||
|
||||
public const string CLIENT_STREAM_NAME = @"lazer";
|
||||
@ -159,7 +161,7 @@ namespace osu.Game
|
||||
/// <summary>
|
||||
/// Mods available for the current <see cref="Ruleset"/>.
|
||||
/// </summary>
|
||||
public readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> AvailableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
|
||||
public readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> AvailableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>(new Dictionary<ModType, IReadOnlyList<Mod>>());
|
||||
|
||||
private BeatmapDifficultyCache difficultyCache;
|
||||
|
||||
@ -511,21 +513,36 @@ namespace osu.Game
|
||||
if (instance == null)
|
||||
{
|
||||
// reject the change if the ruleset is not available.
|
||||
Ruleset.Value = r.OldValue?.Available == true ? r.OldValue : RulesetStore.AvailableRulesets.First();
|
||||
revertRulesetChange();
|
||||
return;
|
||||
}
|
||||
|
||||
var dict = new Dictionary<ModType, IReadOnlyList<Mod>>();
|
||||
|
||||
foreach (ModType type in Enum.GetValues(typeof(ModType)))
|
||||
try
|
||||
{
|
||||
dict[type] = instance.GetModsFor(type).ToList();
|
||||
foreach (ModType type in Enum.GetValues(typeof(ModType)))
|
||||
{
|
||||
dict[type] = instance.GetModsFor(type)
|
||||
// Rulesets should never return null mods, but let's be defensive just in case.
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
|
||||
.Where(mod => mod != null)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, $"Could not load mods for \"{instance.RulesetInfo.Name}\" ruleset. Current ruleset has been rolled back.");
|
||||
revertRulesetChange();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SelectedMods.Disabled)
|
||||
SelectedMods.Value = Array.Empty<Mod>();
|
||||
|
||||
AvailableMods.Value = dict;
|
||||
|
||||
void revertRulesetChange() => Ruleset.Value = r.OldValue?.Available == true ? r.OldValue : RulesetStore.AvailableRulesets.First();
|
||||
}
|
||||
|
||||
private int allowableExceptions;
|
||||
|
@ -165,7 +165,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
},
|
||||
new UpdateableFlag(score.User.Country)
|
||||
{
|
||||
Size = new Vector2(19, 13),
|
||||
Size = new Vector2(19, 14),
|
||||
ShowPlaceholderOnNull = false,
|
||||
},
|
||||
username,
|
||||
|
@ -116,7 +116,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Size = new Vector2(19, 13),
|
||||
Size = new Vector2(19, 14),
|
||||
Margin = new MarginPadding { Top = 3 }, // makes spacing look more even
|
||||
ShowPlaceholderOnNull = false,
|
||||
},
|
||||
|
@ -8,7 +8,8 @@ namespace osu.Game.Overlays.Dialog
|
||||
{
|
||||
public class PopupDialogButton : DialogButton
|
||||
{
|
||||
public PopupDialogButton()
|
||||
public PopupDialogButton(HoverSampleSet sampleSet = HoverSampleSet.Button)
|
||||
: base(sampleSet)
|
||||
{
|
||||
Height = 50;
|
||||
BackgroundColour = Color4Extensions.FromHex(@"150e14");
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Overlays.Dialog
|
||||
{
|
||||
@ -13,5 +14,10 @@ namespace osu.Game.Overlays.Dialog
|
||||
{
|
||||
ButtonColour = colours.Blue;
|
||||
}
|
||||
|
||||
public PopupDialogCancelButton()
|
||||
: base(HoverSampleSet.DialogCancel)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,13 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Audio.Effects;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
|
||||
@ -47,6 +51,33 @@ namespace osu.Game.Overlays.Dialog
|
||||
{
|
||||
}
|
||||
|
||||
private Sample tickSample;
|
||||
private Sample confirmSample;
|
||||
private double lastTickPlaybackTime;
|
||||
private AudioFilter lowPassFilter = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
tickSample = audio.Samples.Get(@"UI/dialog-dangerous-tick");
|
||||
confirmSample = audio.Samples.Get(@"UI/dialog-dangerous-select");
|
||||
|
||||
AddInternal(lowPassFilter = new AudioFilter(audio.SampleMixer));
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
Progress.BindValueChanged(progressChanged);
|
||||
}
|
||||
|
||||
protected override void Confirm()
|
||||
{
|
||||
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
|
||||
confirmSample?.Play();
|
||||
base.Confirm();
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
BeginConfirm();
|
||||
@ -56,7 +87,28 @@ namespace osu.Game.Overlays.Dialog
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
if (!e.HasAnyButtonPressed)
|
||||
{
|
||||
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
|
||||
AbortConfirm();
|
||||
}
|
||||
}
|
||||
|
||||
private void progressChanged(ValueChangedEvent<double> progress)
|
||||
{
|
||||
if (progress.NewValue < progress.OldValue) return;
|
||||
|
||||
if (Clock.CurrentTime - lastTickPlaybackTime < 30) return;
|
||||
|
||||
lowPassFilter.CutoffTo((int)(progress.NewValue * AudioFilter.MAX_LOWPASS_CUTOFF * 0.5));
|
||||
|
||||
var channel = tickSample.GetChannel();
|
||||
|
||||
channel.Frequency.Value = 1 + progress.NewValue * 0.5f;
|
||||
channel.Volume.Value = 0.5f + progress.NewValue / 2f;
|
||||
|
||||
channel.Play();
|
||||
|
||||
lastTickPlaybackTime = Clock.CurrentTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Overlays.Dialog
|
||||
{
|
||||
@ -13,5 +14,10 @@ namespace osu.Game.Overlays.Dialog
|
||||
{
|
||||
ButtonColour = colours.Pink;
|
||||
}
|
||||
|
||||
public PopupDialogOkButton()
|
||||
: base(HoverSampleSet.DialogOk)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -133,7 +133,7 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
{
|
||||
userFlag = new UpdateableFlag
|
||||
{
|
||||
Size = new Vector2(30, 20),
|
||||
Size = new Vector2(28, 20),
|
||||
ShowPlaceholderOnNull = false,
|
||||
},
|
||||
userCountryText = new OsuSpriteText
|
||||
|
@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Rankings
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(30, 20)
|
||||
Size = new Vector2(28, 20)
|
||||
},
|
||||
countryName = new OsuSpriteText
|
||||
{
|
||||
|
@ -97,7 +97,7 @@ namespace osu.Game.Overlays.Rankings.Tables
|
||||
{
|
||||
new UpdateableFlag(GetCountry(item))
|
||||
{
|
||||
Size = new Vector2(30, 20),
|
||||
Size = new Vector2(28, 20),
|
||||
ShowPlaceholderOnNull = false,
|
||||
},
|
||||
CreateFlagContent(item)
|
||||
|
@ -2,24 +2,131 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Transforms;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Overlays.Toolbar
|
||||
{
|
||||
public class ToolbarMusicButton : ToolbarOverlayToggleButton
|
||||
{
|
||||
private Circle volumeBar;
|
||||
|
||||
protected override Anchor TooltipAnchor => Anchor.TopRight;
|
||||
|
||||
public ToolbarMusicButton()
|
||||
{
|
||||
Hotkey = GlobalAction.ToggleNowPlaying;
|
||||
AutoSizeAxes = Axes.X;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(NowPlayingOverlay music)
|
||||
{
|
||||
StateContainer = music;
|
||||
|
||||
Flow.Padding = new MarginPadding { Horizontal = Toolbar.HEIGHT / 4 };
|
||||
Flow.Add(volumeDisplay = new Container
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Width = 3f,
|
||||
Height = IconContainer.Height,
|
||||
Margin = new MarginPadding { Horizontal = 2.5f },
|
||||
Masking = true,
|
||||
Children = new[]
|
||||
{
|
||||
new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.White.Opacity(0.25f),
|
||||
},
|
||||
volumeBar = new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0f,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Colour = Color4.White,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audio { get; set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private VolumeOverlay volume { get; set; }
|
||||
|
||||
private IBindable<double> globalVolume;
|
||||
private Container volumeDisplay;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
globalVolume = audio.Volume.GetBoundCopy();
|
||||
globalVolume.BindValueChanged(v => volumeBar.ResizeHeightTo((float)v.NewValue, 200, Easing.OutQuint), true);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Up:
|
||||
focusForAdjustment();
|
||||
volume?.Adjust(GlobalAction.IncreaseVolume);
|
||||
return true;
|
||||
|
||||
case Key.Down:
|
||||
focusForAdjustment();
|
||||
volume?.Adjust(GlobalAction.DecreaseVolume);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
protected override bool OnScroll(ScrollEvent e)
|
||||
{
|
||||
focusForAdjustment();
|
||||
volume?.Adjust(GlobalAction.IncreaseVolume, e.ScrollDelta.Y, e.IsPrecise);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void focusForAdjustment()
|
||||
{
|
||||
volume?.FocusMasterVolume();
|
||||
expandVolumeBarTemporarily();
|
||||
}
|
||||
|
||||
private TransformSequence<Container> expandTransform;
|
||||
private ScheduledDelegate contractTransform;
|
||||
|
||||
private void expandVolumeBarTemporarily()
|
||||
{
|
||||
// avoid starting a new transform if one is already active.
|
||||
if (expandTransform == null)
|
||||
{
|
||||
expandTransform = volumeDisplay.ResizeWidthTo(6, 500, Easing.OutQuint);
|
||||
expandTransform.Finally(_ => expandTransform = null);
|
||||
}
|
||||
|
||||
contractTransform?.Cancel();
|
||||
contractTransform = Scheduler.AddDelayed(() =>
|
||||
{
|
||||
volumeDisplay.ResizeWidthTo(3f, 500, Easing.OutQuint);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -140,11 +140,16 @@ namespace osu.Game.Overlays
|
||||
|
||||
private ScheduledDelegate popOutDelegate;
|
||||
|
||||
public void FocusMasterVolume()
|
||||
{
|
||||
volumeMeters.Select(volumeMeterMaster);
|
||||
}
|
||||
|
||||
public override void Show()
|
||||
{
|
||||
// Focus on the master meter as a default if previously hidden
|
||||
if (State.Value == Visibility.Hidden)
|
||||
volumeMeters.Select(volumeMeterMaster);
|
||||
FocusMasterVolume();
|
||||
|
||||
if (State.Value == Visibility.Visible)
|
||||
schedulePopOut();
|
||||
|
@ -93,6 +93,15 @@ namespace osu.Game.Rulesets
|
||||
return AllMods.FirstOrDefault(m => m is T)?.CreateInstance() as T;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an enumerable with mods that are supported by the ruleset for the supplied <paramref name="type"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If there are no applicable mods from the given <paramref name="type"/> in this ruleset,
|
||||
/// then the proper behaviour is to return an empty enumerable.
|
||||
/// <see langword="null"/> mods should not be present in the returned enumerable.
|
||||
/// </remarks>
|
||||
[ItemNotNull]
|
||||
public abstract IEnumerable<Mod> GetModsFor(ModType type);
|
||||
|
||||
/// <summary>
|
||||
|
@ -13,14 +13,13 @@ using osu.Game.Database;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Stores;
|
||||
using Realms;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Scoring
|
||||
{
|
||||
public class ScoreModelManager : RealmArchiveModelManager<ScoreInfo>
|
||||
public class ScoreImporter : RealmArchiveModelImporter<ScoreInfo>
|
||||
{
|
||||
public override IEnumerable<string> HandledExtensions => new[] { ".osr" };
|
||||
|
||||
@ -29,7 +28,7 @@ namespace osu.Game.Scoring
|
||||
private readonly RulesetStore rulesets;
|
||||
private readonly Func<BeatmapManager> beatmaps;
|
||||
|
||||
public ScoreModelManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmAccess realm)
|
||||
public ScoreImporter(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmAccess realm)
|
||||
: base(storage, realm)
|
||||
{
|
||||
this.rulesets = rulesets;
|
||||
@ -71,10 +70,5 @@ namespace osu.Game.Scoring
|
||||
if (string.IsNullOrEmpty(model.StatisticsJson))
|
||||
model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics);
|
||||
}
|
||||
|
||||
public override bool IsAvailableLocally(ScoreInfo model)
|
||||
{
|
||||
return Realm.Run(realm => realm.All<ScoreInfo>().Any(s => s.OnlineID == model.OnlineID));
|
||||
}
|
||||
}
|
||||
}
|
@ -22,26 +22,28 @@ using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Scoring
|
||||
{
|
||||
public class ScoreManager : IModelManager<ScoreInfo>, IModelImporter<ScoreInfo>
|
||||
public class ScoreManager : ModelManager<ScoreInfo>, IModelImporter<ScoreInfo>
|
||||
{
|
||||
private readonly RealmAccess realm;
|
||||
private readonly Scheduler scheduler;
|
||||
private readonly Func<BeatmapDifficultyCache> difficulties;
|
||||
private readonly OsuConfigManager configManager;
|
||||
private readonly ScoreModelManager scoreModelManager;
|
||||
private readonly ScoreImporter scoreImporter;
|
||||
|
||||
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmAccess realm, Scheduler scheduler,
|
||||
Func<BeatmapDifficultyCache> difficulties = null, OsuConfigManager configManager = null)
|
||||
: base(storage, realm)
|
||||
{
|
||||
this.realm = realm;
|
||||
this.scheduler = scheduler;
|
||||
this.difficulties = difficulties;
|
||||
this.configManager = configManager;
|
||||
|
||||
scoreModelManager = new ScoreModelManager(rulesets, beatmaps, storage, realm);
|
||||
scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm)
|
||||
{
|
||||
PostNotification = obj => PostNotification?.Invoke(obj)
|
||||
};
|
||||
}
|
||||
|
||||
public Score GetScore(ScoreInfo score) => scoreModelManager.GetScore(score);
|
||||
public Score GetScore(ScoreInfo score) => scoreImporter.GetScore(score);
|
||||
|
||||
/// <summary>
|
||||
/// Perform a lookup query on available <see cref="ScoreInfo"/>s.
|
||||
@ -50,7 +52,7 @@ namespace osu.Game.Scoring
|
||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||
public ScoreInfo Query(Expression<Func<ScoreInfo, bool>> query)
|
||||
{
|
||||
return realm.Run(r => r.All<ScoreInfo>().FirstOrDefault(query)?.Detach());
|
||||
return Realm.Run(r => r.All<ScoreInfo>().FirstOrDefault(query)?.Detach());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -227,25 +229,9 @@ namespace osu.Game.Scoring
|
||||
}
|
||||
}
|
||||
|
||||
#region Implementation of IPostNotifications
|
||||
|
||||
public Action<Notification> PostNotification
|
||||
{
|
||||
set => scoreModelManager.PostNotification = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IModelManager<ScoreInfo>
|
||||
|
||||
public bool Delete(ScoreInfo item)
|
||||
{
|
||||
return scoreModelManager.Delete(item);
|
||||
}
|
||||
|
||||
public void Delete([CanBeNull] Expression<Func<ScoreInfo, bool>> filter = null, bool silent = false)
|
||||
{
|
||||
realm.Run(r =>
|
||||
Realm.Run(r =>
|
||||
{
|
||||
var items = r.All<ScoreInfo>()
|
||||
.Where(s => !s.DeletePending);
|
||||
@ -253,44 +239,37 @@ namespace osu.Game.Scoring
|
||||
if (filter != null)
|
||||
items = items.Where(filter);
|
||||
|
||||
scoreModelManager.Delete(items.ToList(), silent);
|
||||
Delete(items.ToList(), silent);
|
||||
});
|
||||
}
|
||||
|
||||
public void Delete(BeatmapInfo beatmap, bool silent = false)
|
||||
{
|
||||
realm.Run(r =>
|
||||
Realm.Run(r =>
|
||||
{
|
||||
var beatmapScores = r.Find<BeatmapInfo>(beatmap.ID).Scores.ToList();
|
||||
scoreModelManager.Delete(beatmapScores, silent);
|
||||
Delete(beatmapScores, silent);
|
||||
});
|
||||
}
|
||||
|
||||
public void Delete(List<ScoreInfo> items, bool silent = false) => scoreModelManager.Delete(items, silent);
|
||||
public Task Import(params string[] paths) => scoreImporter.Import(paths);
|
||||
|
||||
public void Undelete(List<ScoreInfo> items, bool silent = false) => scoreModelManager.Undelete(items, silent);
|
||||
public Task Import(params ImportTask[] tasks) => scoreImporter.Import(tasks);
|
||||
|
||||
public void Undelete(ScoreInfo item) => scoreModelManager.Undelete(item);
|
||||
public override bool IsAvailableLocally(ScoreInfo model) => Realm.Run(realm => realm.All<ScoreInfo>().Any(s => s.OnlineID == model.OnlineID));
|
||||
|
||||
public Task Import(params string[] paths) => scoreModelManager.Import(paths);
|
||||
public IEnumerable<string> HandledExtensions => scoreImporter.HandledExtensions;
|
||||
|
||||
public Task Import(params ImportTask[] tasks) => scoreModelManager.Import(tasks);
|
||||
public Task<IEnumerable<Live<ScoreInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => scoreImporter.Import(notification, tasks);
|
||||
|
||||
public IEnumerable<string> HandledExtensions => scoreModelManager.HandledExtensions;
|
||||
|
||||
public Task<IEnumerable<Live<ScoreInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => scoreModelManager.Import(notification, tasks);
|
||||
|
||||
public Live<ScoreInfo> Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) => scoreModelManager.Import(item, archive, cancellationToken);
|
||||
|
||||
public bool IsAvailableLocally(ScoreInfo model) => scoreModelManager.IsAvailableLocally(model);
|
||||
|
||||
#endregion
|
||||
public Live<ScoreInfo> Import(ScoreInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default) =>
|
||||
scoreImporter.Import(item, archive, batchImport, cancellationToken);
|
||||
|
||||
#region Implementation of IPresentImports<ScoreInfo>
|
||||
|
||||
public Action<IEnumerable<Live<ScoreInfo>>> PostImport
|
||||
{
|
||||
set => scoreModelManager.PostImport = value;
|
||||
set => scoreImporter.PostImport = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -106,6 +106,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// </summary>
|
||||
protected virtual bool AllowDeselectionDuringDrag => true;
|
||||
|
||||
/// <remarks>
|
||||
/// Positional input must be received outside the container's bounds,
|
||||
/// in order to handle blueprints which are partially offscreen.
|
||||
/// </remarks>
|
||||
/// <seealso cref="SelectionHandler{T}.ReceivePositionalInputAt"/>
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
bool selectionPerformed = performMouseDownActions(e);
|
||||
|
@ -30,8 +30,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// </summary>
|
||||
public class ComposeBlueprintContainer : EditorBlueprintContainer
|
||||
{
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
|
||||
private readonly Container<PlacementBlueprint> placementBlueprintContainer;
|
||||
|
||||
protected new EditorSelectionHandler SelectionHandler => (EditorSelectionHandler)base.SelectionHandler;
|
||||
|
@ -97,6 +97,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
#region User Input Handling
|
||||
|
||||
/// <remarks>
|
||||
/// Positional input must be received outside the container's bounds,
|
||||
/// in order to handle blueprints which are partially offscreen.
|
||||
/// </remarks>
|
||||
/// <seealso cref="BlueprintContainer{T}.ReceivePositionalInputAt"/>
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the selected items being moved.
|
||||
/// </summary>
|
||||
|
@ -33,9 +33,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
private Bindable<HitObject> placement;
|
||||
private SelectionBlueprint<HitObject> placementBlueprint;
|
||||
|
||||
// We want children within the timeline to be interactable
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => timeline.ScreenSpaceDrawQuad.Contains(screenSpacePos);
|
||||
|
||||
public TimelineBlueprintContainer(HitObjectComposer composer)
|
||||
: base(composer)
|
||||
{
|
||||
|
@ -6,23 +6,16 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
internal class TimelineSelectionHandler : EditorSelectionHandler
|
||||
{
|
||||
[Resolved]
|
||||
private Timeline timeline { get; set; }
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => timeline.ScreenSpaceDrawQuad.Contains(screenSpacePos);
|
||||
|
||||
// for now we always allow movement. snapping is provided by the Timeline's "distance" snap implementation
|
||||
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent) => true;
|
||||
|
||||
|
@ -66,8 +66,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
minZoom = value;
|
||||
|
||||
if (Zoom < value)
|
||||
Zoom = value;
|
||||
// ensure zoom range is in valid state before updating zoom.
|
||||
if (MinZoom < MaxZoom)
|
||||
updateZoom();
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,8 +87,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
maxZoom = value;
|
||||
|
||||
if (Zoom > value)
|
||||
Zoom = value;
|
||||
// ensure zoom range is in valid state before updating zoom.
|
||||
if (MaxZoom > MinZoom)
|
||||
updateZoom();
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,15 +99,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
public float Zoom
|
||||
{
|
||||
get => zoomTarget;
|
||||
set
|
||||
{
|
||||
value = Math.Clamp(value, MinZoom, MaxZoom);
|
||||
set => updateZoom(value);
|
||||
}
|
||||
|
||||
if (IsLoaded)
|
||||
setZoomTarget(value, ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), zoomedContent).X);
|
||||
else
|
||||
currentZoom = zoomTarget = value;
|
||||
}
|
||||
private void updateZoom(float? value = null)
|
||||
{
|
||||
float newZoom = Math.Clamp(value ?? Zoom, MinZoom, MaxZoom);
|
||||
|
||||
if (IsLoaded)
|
||||
setZoomTarget(newZoom, ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), zoomedContent).X);
|
||||
else
|
||||
currentZoom = zoomTarget = newZoom;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
@ -63,6 +63,8 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
public override bool? AllowTrackAdjustments => false;
|
||||
|
||||
protected override bool PlayExitSound => !ExitConfirmed && !switchingDifficulty;
|
||||
|
||||
protected bool HasUnsavedChanges
|
||||
{
|
||||
get
|
||||
@ -99,6 +101,8 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
protected bool ExitConfirmed { get; private set; }
|
||||
|
||||
private bool switchingDifficulty;
|
||||
|
||||
private string lastSavedHash;
|
||||
|
||||
private Container<EditorScreen> screenContainer;
|
||||
@ -857,7 +861,10 @@ namespace osu.Game.Screens.Edit
|
||||
}
|
||||
|
||||
private void switchToNewDifficulty(RulesetInfo rulesetInfo, bool createCopy)
|
||||
=> loader?.ScheduleSwitchToNewDifficulty(editorBeatmap.BeatmapInfo, rulesetInfo, createCopy, GetState(rulesetInfo));
|
||||
{
|
||||
switchingDifficulty = true;
|
||||
loader?.ScheduleSwitchToNewDifficulty(editorBeatmap.BeatmapInfo, rulesetInfo, createCopy, GetState(rulesetInfo));
|
||||
}
|
||||
|
||||
private EditorMenuItem createDifficultySwitchMenu()
|
||||
{
|
||||
|
@ -63,6 +63,8 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
protected virtual string SeeyaSampleName => "Intro/seeya";
|
||||
|
||||
protected override bool PlayExitSound => false;
|
||||
|
||||
private LeasedBindable<WorkingBeatmap> beatmap;
|
||||
|
||||
private OsuScreen nextScreen;
|
||||
|
@ -66,6 +66,8 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
protected override BackgroundScreen CreateBackground() => background;
|
||||
|
||||
protected override bool PlayExitSound => false;
|
||||
|
||||
private Bindable<double> holdDelay;
|
||||
private Bindable<bool> loginDisplayed;
|
||||
|
||||
|
@ -37,6 +37,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
|
||||
{
|
||||
public override string Title => "Lounge";
|
||||
|
||||
protected override bool PlayExitSound => false;
|
||||
|
||||
protected override BackgroundScreen CreateBackground() => new LoungeBackgroundScreen
|
||||
{
|
||||
SelectedRoom = { BindTarget = SelectedRoom }
|
||||
|
@ -41,6 +41,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
|
||||
public override string ShortTitle => "room";
|
||||
|
||||
protected override bool PlayExitSound => !exitConfirmed;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; }
|
||||
|
||||
|
@ -125,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Size = new Vector2(30, 20),
|
||||
Size = new Vector2(28, 20),
|
||||
Country = user?.Country
|
||||
},
|
||||
new OsuSpriteText
|
||||
|
@ -77,7 +77,7 @@ namespace osu.Game.Screens
|
||||
|
||||
private Sample sampleExit;
|
||||
|
||||
protected virtual bool PlayResumeSound => true;
|
||||
protected virtual bool PlayExitSound => true;
|
||||
|
||||
public virtual float BackgroundParallaxAmount => 1;
|
||||
|
||||
@ -173,9 +173,6 @@ namespace osu.Game.Screens
|
||||
|
||||
public override void OnResuming(ScreenTransitionEvent e)
|
||||
{
|
||||
if (PlayResumeSound)
|
||||
sampleExit?.Play();
|
||||
|
||||
applyArrivingDefaults(true);
|
||||
|
||||
// it's feasible to resume to a screen if the target screen never loaded successfully.
|
||||
@ -215,6 +212,9 @@ namespace osu.Game.Screens
|
||||
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
{
|
||||
if (ValidForResume && PlayExitSound)
|
||||
sampleExit?.Play();
|
||||
|
||||
if (ValidForResume && logo != null)
|
||||
onExitingLogo();
|
||||
|
||||
|
@ -53,6 +53,8 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public override bool AllowBackButton => false; // handled by HoldForMenuButton
|
||||
|
||||
protected override bool PlayExitSound => !isRestarting;
|
||||
|
||||
protected override UserActivity InitialActivity => new UserActivity.InSoloGame(Beatmap.Value.BeatmapInfo, Ruleset.Value);
|
||||
|
||||
public override float BackgroundParallaxAmount => 0.1f;
|
||||
@ -75,6 +77,8 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public Action RestartRequested;
|
||||
|
||||
private bool isRestarting;
|
||||
|
||||
private Bindable<bool> mouseWheelDisabled;
|
||||
|
||||
private readonly Bindable<bool> storyboardReplacesBackground = new Bindable<bool>();
|
||||
@ -643,6 +647,8 @@ namespace osu.Game.Screens.Play
|
||||
if (!Configuration.AllowRestart)
|
||||
return;
|
||||
|
||||
isRestarting = true;
|
||||
|
||||
// at the point of restarting the track should either already be paused or the volume should be zero.
|
||||
// stopping here is to ensure music doesn't become audible after exiting back to PlayerLoader.
|
||||
musicController.Stop();
|
||||
|
@ -51,8 +51,6 @@ namespace osu.Game.Screens.Play
|
||||
// We show the previous screen status
|
||||
protected override UserActivity? InitialActivity => null;
|
||||
|
||||
protected override bool PlayResumeSound => false;
|
||||
|
||||
protected BeatmapMetadataDisplay MetadataInfo { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
|
@ -14,24 +14,27 @@ using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Stores;
|
||||
using Realms;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
public class SkinModelManager : RealmArchiveModelManager<SkinInfo>
|
||||
public class SkinImporter : RealmArchiveModelImporter<SkinInfo>
|
||||
{
|
||||
private const string skin_info_file = "skininfo.json";
|
||||
|
||||
private readonly IStorageResourceProvider skinResources;
|
||||
|
||||
public SkinModelManager(Storage storage, RealmAccess realm, IStorageResourceProvider skinResources)
|
||||
private readonly ModelManager<SkinInfo> modelManager;
|
||||
|
||||
public SkinImporter(Storage storage, RealmAccess realm, IStorageResourceProvider skinResources)
|
||||
: base(storage, realm)
|
||||
{
|
||||
this.skinResources = skinResources;
|
||||
|
||||
modelManager = new ModelManager<SkinInfo>(storage, realm);
|
||||
|
||||
// can be removed 20220420.
|
||||
populateMissingHashes();
|
||||
}
|
||||
@ -46,8 +49,6 @@ namespace osu.Game.Skinning
|
||||
|
||||
private const string unknown_creator_string = @"Unknown";
|
||||
|
||||
protected override bool HasCustomHashFunction => true;
|
||||
|
||||
protected override void Populate(SkinInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var skinInfoFile = model.Files.SingleOrDefault(f => f.Filename == skin_info_file);
|
||||
@ -157,7 +158,7 @@ namespace osu.Game.Skinning
|
||||
sw.WriteLine(line);
|
||||
}
|
||||
|
||||
ReplaceFile(existingFile, stream, realm);
|
||||
modelManager.ReplaceFile(existingFile, stream, realm);
|
||||
|
||||
// can be removed 20220502.
|
||||
if (!ensureIniWasUpdated(item))
|
||||
@ -187,7 +188,7 @@ namespace osu.Game.Skinning
|
||||
sw.WriteLine(line);
|
||||
}
|
||||
|
||||
AddFile(item, stream, @"skin.ini", realm);
|
||||
modelManager.AddFile(item, stream, @"skin.ini", realm);
|
||||
}
|
||||
|
||||
item.Hash = ComputeHash(item);
|
||||
@ -219,7 +220,7 @@ namespace osu.Game.Skinning
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Delete(skin);
|
||||
modelManager.Delete(skin);
|
||||
Logger.Error(e, $"Existing skin {skin} has been deleted during hash recomputation due to being invalid");
|
||||
}
|
||||
}
|
||||
@ -237,7 +238,7 @@ namespace osu.Game.Skinning
|
||||
|
||||
using (var streamContent = new MemoryStream(Encoding.UTF8.GetBytes(skinInfoJson)))
|
||||
{
|
||||
AddFile(s, streamContent, skin_info_file, s.Realm);
|
||||
modelManager.AddFile(s, streamContent, skin_info_file, s.Realm);
|
||||
}
|
||||
|
||||
// Then serialise each of the drawable component groups into respective files.
|
||||
@ -252,16 +253,14 @@ namespace osu.Game.Skinning
|
||||
var oldFile = s.Files.FirstOrDefault(f => f.Filename == filename);
|
||||
|
||||
if (oldFile != null)
|
||||
ReplaceFile(oldFile, streamContent, s.Realm);
|
||||
modelManager.ReplaceFile(oldFile, streamContent, s.Realm);
|
||||
else
|
||||
AddFile(s, streamContent, filename, s.Realm);
|
||||
modelManager.AddFile(s, streamContent, filename, s.Realm);
|
||||
}
|
||||
}
|
||||
|
||||
s.Hash = ComputeHash(s);
|
||||
});
|
||||
}
|
||||
|
||||
public override bool IsAvailableLocally(SkinInfo model) => true; // skins do not have online download support yet.
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading;
|
||||
@ -24,7 +23,6 @@ using osu.Game.Audio;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Utils;
|
||||
|
||||
@ -38,7 +36,7 @@ namespace osu.Game.Skinning
|
||||
/// For gameplay components, see <see cref="RulesetSkinProvidingContainer"/> which adds extra legacy and toggle logic that may affect the lookup process.
|
||||
/// </remarks>
|
||||
[ExcludeFromDynamicCompile]
|
||||
public class SkinManager : ISkinSource, IStorageResourceProvider, IModelImporter<SkinInfo>, IModelManager<SkinInfo>, IModelFileManager<SkinInfo, RealmNamedFileUsage>
|
||||
public class SkinManager : ModelManager<SkinInfo>, ISkinSource, IStorageResourceProvider, IModelImporter<SkinInfo>
|
||||
{
|
||||
private readonly AudioManager audio;
|
||||
|
||||
@ -55,8 +53,7 @@ namespace osu.Game.Skinning
|
||||
Default = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged()
|
||||
};
|
||||
|
||||
private readonly SkinModelManager skinModelManager;
|
||||
private readonly RealmAccess realm;
|
||||
private readonly SkinImporter skinImporter;
|
||||
|
||||
private readonly IResourceStore<byte[]> userFiles;
|
||||
|
||||
@ -71,8 +68,8 @@ namespace osu.Game.Skinning
|
||||
public Skin DefaultLegacySkin { get; }
|
||||
|
||||
public SkinManager(Storage storage, RealmAccess realm, GameHost host, IResourceStore<byte[]> resources, AudioManager audio, Scheduler scheduler)
|
||||
: base(storage, realm)
|
||||
{
|
||||
this.realm = realm;
|
||||
this.audio = audio;
|
||||
this.scheduler = scheduler;
|
||||
this.host = host;
|
||||
@ -80,7 +77,10 @@ namespace osu.Game.Skinning
|
||||
|
||||
userFiles = new StorageBackedResourceStore(storage.GetStorageForDirectory("files"));
|
||||
|
||||
skinModelManager = new SkinModelManager(storage, realm, this);
|
||||
skinImporter = new SkinImporter(storage, realm, this)
|
||||
{
|
||||
PostNotification = obj => PostNotification?.Invoke(obj),
|
||||
};
|
||||
|
||||
var defaultSkins = new[]
|
||||
{
|
||||
@ -115,7 +115,7 @@ namespace osu.Game.Skinning
|
||||
|
||||
public void SelectRandomSkin()
|
||||
{
|
||||
realm.Run(r =>
|
||||
Realm.Run(r =>
|
||||
{
|
||||
// choose from only user skins, removing the current selection to ensure a new one is chosen.
|
||||
var randomChoices = r.All<SkinInfo>().Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray();
|
||||
@ -128,7 +128,7 @@ namespace osu.Game.Skinning
|
||||
|
||||
var chosen = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length));
|
||||
|
||||
CurrentSkinInfo.Value = chosen.ToLive(realm);
|
||||
CurrentSkinInfo.Value = chosen.ToLive(Realm);
|
||||
});
|
||||
}
|
||||
|
||||
@ -153,7 +153,7 @@ namespace osu.Game.Skinning
|
||||
if (!s.Protected)
|
||||
return false;
|
||||
|
||||
string[] existingSkinNames = realm.Run(r => r.All<SkinInfo>()
|
||||
string[] existingSkinNames = Realm.Run(r => r.All<SkinInfo>()
|
||||
.Where(skin => !skin.DeletePending)
|
||||
.AsEnumerable()
|
||||
.Select(skin => skin.Name).ToArray());
|
||||
@ -166,7 +166,7 @@ namespace osu.Game.Skinning
|
||||
Name = NamingUtils.GetNextBestName(existingSkinNames, $@"{s.Name} (modified)")
|
||||
};
|
||||
|
||||
var result = skinModelManager.Import(skinInfo);
|
||||
var result = skinImporter.Import(skinInfo);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
@ -186,7 +186,7 @@ namespace osu.Game.Skinning
|
||||
if (!skin.SkinInfo.IsManaged)
|
||||
throw new InvalidOperationException($"Attempting to save a skin which is not yet tracked. Call {nameof(EnsureMutableSkin)} first.");
|
||||
|
||||
skinModelManager.Save(skin);
|
||||
skinImporter.Save(skin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -196,7 +196,7 @@ namespace osu.Game.Skinning
|
||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||
public Live<SkinInfo> Query(Expression<Func<SkinInfo, bool>> query)
|
||||
{
|
||||
return realm.Run(r => r.All<SkinInfo>().FirstOrDefault(query)?.ToLive(realm));
|
||||
return Realm.Run(r => r.All<SkinInfo>().FirstOrDefault(query)?.ToLive(Realm));
|
||||
}
|
||||
|
||||
public event Action SourceChanged;
|
||||
@ -251,46 +251,36 @@ namespace osu.Game.Skinning
|
||||
AudioManager IStorageResourceProvider.AudioManager => audio;
|
||||
IResourceStore<byte[]> IStorageResourceProvider.Resources => resources;
|
||||
IResourceStore<byte[]> IStorageResourceProvider.Files => userFiles;
|
||||
RealmAccess IStorageResourceProvider.RealmAccess => realm;
|
||||
RealmAccess IStorageResourceProvider.RealmAccess => Realm;
|
||||
IResourceStore<TextureUpload> IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host.CreateTextureLoaderStore(underlyingStore);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IModelImporter<SkinInfo>
|
||||
|
||||
public Action<Notification> PostNotification
|
||||
{
|
||||
set => skinModelManager.PostNotification = value;
|
||||
}
|
||||
|
||||
public Action<IEnumerable<Live<SkinInfo>>> PostImport
|
||||
{
|
||||
set => skinModelManager.PostImport = value;
|
||||
set => skinImporter.PostImport = value;
|
||||
}
|
||||
|
||||
public Task Import(params string[] paths) => skinModelManager.Import(paths);
|
||||
public Task Import(params string[] paths) => skinImporter.Import(paths);
|
||||
|
||||
public Task Import(params ImportTask[] tasks) => skinModelManager.Import(tasks);
|
||||
public Task Import(params ImportTask[] tasks) => skinImporter.Import(tasks);
|
||||
|
||||
public IEnumerable<string> HandledExtensions => skinModelManager.HandledExtensions;
|
||||
public IEnumerable<string> HandledExtensions => skinImporter.HandledExtensions;
|
||||
|
||||
public Task<IEnumerable<Live<SkinInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => skinModelManager.Import(notification, tasks);
|
||||
public Task<IEnumerable<Live<SkinInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => skinImporter.Import(notification, tasks);
|
||||
|
||||
public Task<Live<SkinInfo>> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) => skinModelManager.Import(task, lowPriority, cancellationToken);
|
||||
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 lowPriority = false, CancellationToken cancellationToken = default) =>
|
||||
skinModelManager.Import(archive, lowPriority, cancellationToken);
|
||||
|
||||
public Live<SkinInfo> Import(SkinInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) =>
|
||||
skinModelManager.Import(item, archive, cancellationToken);
|
||||
public Task<Live<SkinInfo>> Import(ArchiveReader archive, bool batchImport = false, CancellationToken cancellationToken = default) =>
|
||||
skinImporter.Import(archive, batchImport, cancellationToken);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IModelManager<SkinInfo>
|
||||
|
||||
public void Delete([CanBeNull] Expression<Func<SkinInfo, bool>> filter = null, bool silent = false)
|
||||
{
|
||||
realm.Run(r =>
|
||||
Realm.Run(r =>
|
||||
{
|
||||
var items = r.All<SkinInfo>()
|
||||
.Where(s => !s.Protected && !s.DeletePending);
|
||||
@ -303,26 +293,8 @@ namespace osu.Game.Skinning
|
||||
if (items.Any(s => s.ID == currentUserSkin))
|
||||
scheduler.Add(() => CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged());
|
||||
|
||||
skinModelManager.Delete(items.ToList(), silent);
|
||||
Delete(items.ToList(), silent);
|
||||
});
|
||||
}
|
||||
|
||||
public bool Delete(SkinInfo item) => skinModelManager.Delete(item);
|
||||
|
||||
public void Delete(List<SkinInfo> items, bool silent = false) => skinModelManager.Delete(items, silent);
|
||||
|
||||
public void Undelete(List<SkinInfo> items, bool silent = false) => skinModelManager.Undelete(items, silent);
|
||||
|
||||
public void Undelete(SkinInfo item) => skinModelManager.Undelete(item);
|
||||
|
||||
public bool IsAvailableLocally(SkinInfo model) => skinModelManager.IsAvailableLocally(model);
|
||||
|
||||
public void ReplaceFile(SkinInfo model, RealmNamedFileUsage file, Stream contents) => skinModelManager.ReplaceFile(model, file, contents);
|
||||
|
||||
public void DeleteFile(SkinInfo model, RealmNamedFileUsage file) => skinModelManager.DeleteFile(model, file);
|
||||
|
||||
public void AddFile(SkinInfo model, Stream contents, string filename) => skinModelManager.AddFile(model, contents, filename);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Stores;
|
||||
|
||||
namespace osu.Game.Storyboards.Drawables
|
||||
{
|
||||
|
@ -145,11 +145,6 @@ namespace osu.Game.Tests.Visual
|
||||
{
|
||||
}
|
||||
|
||||
protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue)
|
||||
{
|
||||
return new TestBeatmapModelManager(storage, realm, onlineLookupQueue);
|
||||
}
|
||||
|
||||
protected override WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost host)
|
||||
{
|
||||
return new TestWorkingBeatmapCache(this, audioManager, resources, storage, defaultBeatmap, host);
|
||||
@ -181,17 +176,6 @@ namespace osu.Game.Tests.Visual
|
||||
=> testBeatmapManager.TestBeatmap;
|
||||
}
|
||||
|
||||
internal class TestBeatmapModelManager : BeatmapModelManager
|
||||
{
|
||||
public TestBeatmapModelManager(Storage storage, RealmAccess databaseAccess, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue)
|
||||
: base(databaseAccess, storage, beatmapOnlineLookupQueue)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string ComputeHash(BeatmapSetInfo item)
|
||||
=> string.Empty;
|
||||
}
|
||||
|
||||
public override void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null)
|
||||
{
|
||||
// don't actually care about saving for this context.
|
||||
|
@ -53,7 +53,7 @@ namespace osu.Game.Users
|
||||
|
||||
protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country)
|
||||
{
|
||||
Size = new Vector2(39, 26),
|
||||
Size = new Vector2(36, 26),
|
||||
Action = Action,
|
||||
};
|
||||
|
||||
|
@ -36,8 +36,8 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="10.14.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2022.611.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2022.615.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.616.0" />
|
||||
<PackageReference Include="Sentry" Version="3.17.1" />
|
||||
<PackageReference Include="SharpCompress" Version="0.31.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
|
@ -61,8 +61,8 @@
|
||||
<Reference Include="System.Net.Http" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.611.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.615.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.616.0" />
|
||||
</ItemGroup>
|
||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
|
||||
<PropertyGroup>
|
||||
@ -84,7 +84,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2022.611.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2022.615.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.31.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
|
Loading…
Reference in New Issue
Block a user