mirror of
https://github.com/ppy/osu.git
synced 2025-01-26 17:02:57 +08:00
Merge branch 'master' into database-migration-reliability
This commit is contained in:
commit
651862fee0
@ -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>();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,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;
|
||||
@ -82,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;
|
||||
@ -141,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 () =>
|
||||
@ -160,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");
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -170,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;
|
||||
@ -202,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);
|
||||
@ -214,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);
|
||||
@ -232,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);
|
||||
@ -246,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();
|
||||
@ -276,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);
|
||||
@ -296,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();
|
||||
@ -345,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();
|
||||
@ -396,7 +398,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();
|
||||
@ -444,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();
|
||||
@ -492,7 +494,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);
|
||||
@ -527,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();
|
||||
@ -565,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);
|
||||
@ -617,7 +619,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, batchImport: true);
|
||||
@ -644,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);
|
||||
@ -676,7 +678,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);
|
||||
@ -703,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);
|
||||
@ -729,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
|
||||
@ -777,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();
|
||||
@ -794,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();
|
||||
@ -830,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();
|
||||
@ -872,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();
|
||||
@ -923,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();
|
||||
|
@ -210,17 +210,17 @@ 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;
|
||||
}
|
||||
|
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
|
||||
}
|
||||
}
|
56
osu.Game.Tests/Visual/Navigation/TestSceneFirstRunGame.cs
Normal file
56
osu.Game.Tests/Visual/Navigation/TestSceneFirstRunGame.cs
Normal file
@ -0,0 +1,56 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Navigation
|
||||
{
|
||||
[System.ComponentModel.Description("game with first-run setup overlay")]
|
||||
public class TestSceneFirstRunGame : OsuGameTestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddUntilStep("Wait for first-run setup", () => Game.FirstRunOverlay.State.Value == Visibility.Visible);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportantNotificationDoesntInterruptSetup()
|
||||
{
|
||||
AddStep("post important notification", () => Game.Notifications.Post(new SimpleNotification { Text = "Important notification" }));
|
||||
AddAssert("no notification posted", () => Game.Notifications.UnreadCount.Value == 0);
|
||||
AddAssert("first-run setup still visible", () => Game.FirstRunOverlay.State.Value == Visibility.Visible);
|
||||
|
||||
AddUntilStep("finish first-run setup", () =>
|
||||
{
|
||||
Game.FirstRunOverlay.NextButton.TriggerClick();
|
||||
return Game.FirstRunOverlay.State.Value == Visibility.Hidden;
|
||||
});
|
||||
AddWaitStep("wait for post delay", 5);
|
||||
AddAssert("notifications shown", () => Game.Notifications.State.Value == Visibility.Visible);
|
||||
AddAssert("notification posted", () => Game.Notifications.UnreadCount.Value == 1);
|
||||
}
|
||||
|
||||
protected override TestOsuGame CreateTestGame() => new FirstRunGame(LocalStorage, API);
|
||||
|
||||
private class FirstRunGame : TestOsuGame
|
||||
{
|
||||
public FirstRunGame(Storage storage, IAPIProvider api, string[] args = null)
|
||||
: base(storage, api, args)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
LocalConfig.SetValue(OsuSetting.ShowFirstRunSetup, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ namespace osu.Game.Beatmaps
|
||||
/// 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" };
|
||||
|
||||
@ -40,7 +40,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
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;
|
||||
@ -165,11 +165,6 @@ namespace osu.Game.Beatmaps
|
||||
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;
|
||||
@ -23,29 +30,25 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Skinning;
|
||||
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)
|
||||
@ -58,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,
|
||||
@ -99,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");
|
||||
@ -169,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);
|
||||
@ -186,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())
|
||||
{
|
||||
@ -205,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())
|
||||
{
|
||||
@ -220,7 +224,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public void RestoreAll()
|
||||
{
|
||||
realm.Run(r =>
|
||||
Realm.Run(r =>
|
||||
{
|
||||
using (var transaction = r.BeginWrite())
|
||||
{
|
||||
@ -238,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();
|
||||
@ -252,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.
|
||||
@ -279,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 batchImport = false, CancellationToken cancellationToken = default) => beatmapModelManager.Import(task, batchImport, 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 batchImport = false, CancellationToken cancellationToken = default) => beatmapModelManager.Import(archive, batchImport, cancellationToken);
|
||||
public Task<Live<BeatmapSetInfo>?> Import(ArchiveReader archive, bool batchImport = false, CancellationToken cancellationToken = default) =>
|
||||
beatmapImporter.Import(archive, batchImport, cancellationToken);
|
||||
|
||||
public Live<BeatmapSetInfo>? Import(BeatmapSetInfo item, ArchiveReader? archive = null, CancellationToken cancellationToken = default) => beatmapModelManager.Import(item, archive, false, 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
|
||||
|
||||
@ -370,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();
|
||||
|
||||
@ -382,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
|
||||
|
||||
@ -428,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,163 +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.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)
|
||||
{
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ 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
|
||||
{
|
||||
|
@ -15,18 +15,17 @@ using Realms;
|
||||
|
||||
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) =>
|
||||
@ -62,7 +61,7 @@ namespace osu.Game.Database
|
||||
/// <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);
|
||||
}
|
||||
@ -70,7 +69,7 @@ namespace osu.Game.Database
|
||||
/// <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);
|
||||
}
|
||||
@ -78,7 +77,7 @@ namespace osu.Game.Database
|
||||
/// <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));
|
||||
|
||||
@ -200,6 +199,10 @@ namespace osu.Game.Database
|
||||
});
|
||||
}
|
||||
|
||||
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()}";
|
||||
}
|
||||
}
|
@ -72,7 +72,7 @@ namespace osu.Game.Database
|
||||
/// <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)
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -153,7 +153,7 @@ namespace osu.Game
|
||||
|
||||
protected SettingsOverlay Settings;
|
||||
|
||||
private FirstRunSetupOverlay firstRunOverlay;
|
||||
protected FirstRunSetupOverlay FirstRunOverlay { get; private set; }
|
||||
|
||||
private VolumeOverlay volume;
|
||||
|
||||
@ -845,7 +845,7 @@ namespace osu.Game
|
||||
loadComponentSingleFile(CreateUpdateManager(), Add, true);
|
||||
|
||||
// overlay elements
|
||||
loadComponentSingleFile(firstRunOverlay = new FirstRunSetupOverlay(), overlayContent.Add, true);
|
||||
loadComponentSingleFile(FirstRunOverlay = new FirstRunSetupOverlay(), overlayContent.Add, true);
|
||||
loadComponentSingleFile(new ManageCollectionsDialog(), overlayContent.Add, true);
|
||||
loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true);
|
||||
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
|
||||
@ -896,7 +896,7 @@ namespace osu.Game
|
||||
Add(new MusicKeyBindingHandler());
|
||||
|
||||
// side overlays which cancel each other.
|
||||
var singleDisplaySideOverlays = new OverlayContainer[] { Settings, Notifications, firstRunOverlay };
|
||||
var singleDisplaySideOverlays = new OverlayContainer[] { Settings, Notifications, FirstRunOverlay };
|
||||
|
||||
foreach (var overlay in singleDisplaySideOverlays)
|
||||
{
|
||||
@ -921,7 +921,7 @@ namespace osu.Game
|
||||
}
|
||||
|
||||
// ensure only one of these overlays are open at once.
|
||||
var singleDisplayOverlays = new OverlayContainer[] { firstRunOverlay, chatOverlay, news, dashboard, beatmapListing, changelogOverlay, rankingsOverlay, wikiOverlay };
|
||||
var singleDisplayOverlays = new OverlayContainer[] { FirstRunOverlay, chatOverlay, news, dashboard, beatmapListing, changelogOverlay, rankingsOverlay, wikiOverlay };
|
||||
|
||||
foreach (var overlay in singleDisplayOverlays)
|
||||
{
|
||||
|
@ -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;
|
||||
|
||||
@ -489,21 +491,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;
|
||||
|
@ -1,20 +1,22 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using NotificationsStrings = osu.Game.Localisation.NotificationsStrings;
|
||||
|
||||
@ -30,13 +32,15 @@ namespace osu.Game.Overlays
|
||||
|
||||
public const float TRANSITION_LENGTH = 600;
|
||||
|
||||
private FlowContainer<NotificationSection> sections;
|
||||
private FlowContainer<NotificationSection> sections = null!;
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audio { get; set; }
|
||||
private AudioManager audio { get; set; } = null!;
|
||||
|
||||
private readonly IBindable<Visibility> firstRunSetupVisibility = new Bindable<Visibility>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(FirstRunSetupOverlay? firstRunSetup)
|
||||
{
|
||||
X = WIDTH;
|
||||
Width = WIDTH;
|
||||
@ -75,13 +79,16 @@ namespace osu.Game.Overlays
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (firstRunSetup != null)
|
||||
firstRunSetupVisibility.BindTo(firstRunSetup.State);
|
||||
}
|
||||
|
||||
private ScheduledDelegate notificationsEnabler;
|
||||
private ScheduledDelegate? notificationsEnabler;
|
||||
|
||||
private void updateProcessingMode()
|
||||
{
|
||||
bool enabled = OverlayActivationMode.Value == OverlayActivation.All || State.Value == Visibility.Visible;
|
||||
bool enabled = (OverlayActivationMode.Value == OverlayActivation.All && firstRunSetupVisibility.Value != Visibility.Visible) || State.Value == Visibility.Visible;
|
||||
|
||||
notificationsEnabler?.Cancel();
|
||||
|
||||
@ -96,7 +103,8 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
State.ValueChanged += _ => updateProcessingMode();
|
||||
State.BindValueChanged(_ => updateProcessingMode());
|
||||
firstRunSetupVisibility.BindValueChanged(_ => updateProcessingMode());
|
||||
OverlayActivationMode.BindValueChanged(_ => updateProcessingMode(), true);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -19,7 +19,7 @@ using Realms;
|
||||
|
||||
namespace osu.Game.Scoring
|
||||
{
|
||||
public class ScoreModelManager : RealmArchiveModelManager<ScoreInfo>
|
||||
public class ScoreImporter : RealmArchiveModelImporter<ScoreInfo>
|
||||
{
|
||||
public override IEnumerable<string> HandledExtensions => new[] { ".osr" };
|
||||
|
||||
@ -28,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;
|
||||
@ -70,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 batchImport = false, CancellationToken cancellationToken = default) => scoreModelManager.Import(item, archive, batchImport, 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
|
||||
|
@ -20,17 +20,21 @@ using Realms;
|
||||
|
||||
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();
|
||||
}
|
||||
@ -154,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))
|
||||
@ -184,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);
|
||||
@ -216,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");
|
||||
}
|
||||
}
|
||||
@ -234,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.
|
||||
@ -249,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,43 +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 batchImport = false, CancellationToken cancellationToken = default) => skinModelManager.Import(task, batchImport, 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 batchImport = false, CancellationToken cancellationToken = default) =>
|
||||
skinModelManager.Import(archive, batchImport, cancellationToken);
|
||||
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);
|
||||
@ -300,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
|
||||
}
|
||||
}
|
||||
|
@ -145,11 +145,6 @@ namespace osu.Game.Tests.Visual
|
||||
{
|
||||
}
|
||||
|
||||
protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue)
|
||||
{
|
||||
return new BeatmapModelManager(realm, storage, 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);
|
||||
|
@ -132,6 +132,8 @@ namespace osu.Game.Tests.Visual
|
||||
|
||||
public new NotificationOverlay Notifications => base.Notifications;
|
||||
|
||||
public new FirstRunSetupOverlay FirstRunOverlay => base.FirstRunOverlay;
|
||||
|
||||
public new MusicController MusicController => base.MusicController;
|
||||
|
||||
public new OsuConfigManager LocalConfig => base.LocalConfig;
|
||||
|
Loading…
Reference in New Issue
Block a user