1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 17:23:22 +08:00

Merge pull request #18835 from peppy/beatmap-update-flow

Split out beatmap update tasks to `BeatmapUpdater` and invoke from editor save flow
This commit is contained in:
Dan Balasescu 2022-07-01 20:28:04 +09:00 committed by GitHub
commit 3b1842a2c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 277 additions and 126 deletions

View File

@ -35,7 +35,8 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using (var importer = new BeatmapImporter(storage, realm))
var importer = new BeatmapImporter(storage, realm);
using (new RealmRulesetStore(realm, storage))
{
var beatmapSet = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz"));
@ -76,7 +77,8 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using (var importer = new BeatmapImporter(storage, realm))
var importer = new BeatmapImporter(storage, realm);
using (new RealmRulesetStore(realm, storage))
{
var beatmapSet = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz"));
@ -134,7 +136,8 @@ namespace osu.Game.Tests.Database
var manager = new ModelManager<BeatmapSetInfo>(storage, realm);
using (var importer = new BeatmapImporter(storage, realm))
var importer = new BeatmapImporter(storage, realm);
using (new RealmRulesetStore(realm, storage))
{
Task.Run(async () =>
@ -160,7 +163,8 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using (var importer = new BeatmapImporter(storage, realm))
var importer = new BeatmapImporter(storage, realm);
using (new RealmRulesetStore(realm, storage))
{
var imported = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz"));
@ -187,7 +191,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
await LoadOszIntoStore(importer, realm.Realm);
@ -199,7 +203,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@ -217,7 +221,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@ -231,7 +235,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? tempPath = TestResources.GetTestBeatmapForImport();
@ -261,7 +265,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@ -281,7 +285,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@ -317,7 +321,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@ -366,7 +370,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@ -417,7 +421,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@ -465,7 +469,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@ -513,7 +517,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@ -548,7 +552,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var progressNotification = new ImportProgressNotification();
@ -586,7 +590,7 @@ namespace osu.Game.Tests.Database
Interlocked.Increment(ref loggedExceptionCount);
};
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@ -644,7 +648,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm, batchImport: true);
@ -671,7 +675,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(storage, realmFactory);
var importer = new BeatmapImporter(storage, realmFactory);
using var store = new RealmRulesetStore(realmFactory, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Realm);
@ -703,7 +707,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@ -730,7 +734,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@ -756,7 +760,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealm((realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var metadata = new BeatmapMetadata
@ -804,7 +808,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@ -821,7 +825,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@ -857,7 +861,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@ -899,7 +903,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@ -950,7 +954,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using var importer = new BeatmapImporter(storage, realm);
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();

View File

@ -212,17 +212,17 @@ namespace osu.Game.Tests.Online
{
}
protected override BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue)
protected override BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapUpdater beatmapUpdater)
{
return new TestBeatmapImporter(this, storage, realm, onlineLookupQueue);
return new TestBeatmapImporter(this, storage, realm, beatmapUpdater);
}
internal class TestBeatmapImporter : BeatmapImporter
{
private readonly TestBeatmapManager testBeatmapManager;
public TestBeatmapImporter(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue)
: base(storage, databaseAccess, beatmapOnlineLookupQueue)
public TestBeatmapImporter(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess, BeatmapUpdater beatmapUpdater)
: base(storage, databaseAccess, beatmapUpdater)
{
this.testBeatmapManager = testBeatmapManager;
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
@ -130,6 +131,54 @@ namespace osu.Game.Tests.Visual.Editing
!ReferenceEquals(EditorBeatmap.HitObjects[0].DifficultyControlPoint, DifficultyControlPoint.DEFAULT));
}
[Test]
public void TestLengthAndStarRatingUpdated()
{
WorkingBeatmap working = null;
double lastStarRating = 0;
double lastLength = 0;
AddStep("Add timing point", () => EditorBeatmap.ControlPointInfo.Add(500, new TimingControlPoint()));
AddStep("Change to placement mode", () => InputManager.Key(Key.Number2));
AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre));
AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left));
AddAssert("One hitobject placed", () => EditorBeatmap.HitObjects.Count == 1);
SaveEditor();
AddStep("Get working beatmap", () => working = Game.BeatmapManager.GetWorkingBeatmap(EditorBeatmap.BeatmapInfo, true));
AddAssert("Beatmap length is zero", () => working.BeatmapInfo.Length == 0);
checkDifficultyIncreased();
AddStep("Move forward", () => InputManager.Key(Key.Right));
AddStep("Place another hitcircle", () => InputManager.Click(MouseButton.Left));
AddAssert("Two hitobjects placed", () => EditorBeatmap.HitObjects.Count == 2);
SaveEditor();
AddStep("Get working beatmap", () => working = Game.BeatmapManager.GetWorkingBeatmap(EditorBeatmap.BeatmapInfo, true));
checkDifficultyIncreased();
checkLengthIncreased();
void checkLengthIncreased()
{
AddStep("Beatmap length increased", () =>
{
Assert.That(working.BeatmapInfo.Length, Is.GreaterThan(lastLength));
lastLength = working.BeatmapInfo.Length;
});
}
void checkDifficultyIncreased()
{
AddStep("Beatmap difficulty increased", () =>
{
Assert.That(working.BeatmapInfo.StarRating, Is.GreaterThan(lastStarRating));
lastStarRating = working.BeatmapInfo.StarRating;
});
}
}
[Test]
public void TestExitWithoutSaveFromExistingBeatmap()
{

View File

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@ -166,15 +167,22 @@ namespace osu.Game.Tests.Visual.SongSelect
var beatmapSet = TestResources.CreateTestBeatmapSetInfo(rulesets.Length, rulesets);
var importedBeatmapSet = Game.BeatmapManager.Import(beatmapSet);
Debug.Assert(importedBeatmapSet != null);
importedBeatmapSet.PerformWrite(s =>
{
for (int i = 0; i < rulesets.Length; i++)
{
var beatmap = beatmapSet.Beatmaps[i];
var beatmap = s.Beatmaps[i];
beatmap.StarRating = i + 1;
beatmap.DifficultyName = $"SR{i + 1}";
}
});
return Game.BeatmapManager.Import(beatmapSet)?.Value;
return importedBeatmapSet.Value;
}
private bool ensureAllBeatmapSetsImported(IEnumerable<BeatmapSetInfo> beatmapSets) => beatmapSets.All(set => set != null);

View File

@ -81,6 +81,11 @@ namespace osu.Game.Beatmaps
}, true);
}
public void Invalidate(IBeatmapInfo beatmap)
{
base.Invalidate(lookup => lookup.BeatmapInfo.Equals(beatmap));
}
/// <summary>
/// Retrieves a bindable containing the star difficulty of a <see cref="BeatmapInfo"/> that follows the currently-selected ruleset and mods.
/// </summary>

View File

@ -6,10 +6,8 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Testing;
@ -19,8 +17,6 @@ using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Skinning;
using Realms;
namespace osu.Game.Beatmaps
@ -29,18 +25,18 @@ namespace osu.Game.Beatmaps
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
/// </summary>
[ExcludeFromDynamicCompile]
public class BeatmapImporter : RealmArchiveModelImporter<BeatmapSetInfo>, IDisposable
public class BeatmapImporter : RealmArchiveModelImporter<BeatmapSetInfo>
{
public override IEnumerable<string> HandledExtensions => new[] { ".osz" };
protected override string[] HashableFileTypes => new[] { ".osu" };
private readonly BeatmapOnlineLookupQueue? onlineLookupQueue;
private readonly BeatmapUpdater? beatmapUpdater;
public BeatmapImporter(Storage storage, RealmAccess realm, BeatmapOnlineLookupQueue? onlineLookupQueue = null)
public BeatmapImporter(Storage storage, RealmAccess realm, BeatmapUpdater? beatmapUpdater = null)
: base(storage, realm)
{
this.onlineLookupQueue = onlineLookupQueue;
this.beatmapUpdater = beatmapUpdater;
}
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz";
@ -64,8 +60,7 @@ namespace osu.Game.Beatmaps
bool hadOnlineIDs = beatmapSet.Beatmaps.Any(b => b.OnlineID > 0);
onlineLookupQueue?.Update(beatmapSet);
// TODO: this may no longer be valid as we aren't doing an online population at this point.
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
if (hadOnlineIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineID > 0))
{
@ -101,6 +96,13 @@ namespace osu.Game.Beatmaps
}
}
protected override void PostImport(BeatmapSetInfo model, Realm realm)
{
base.PostImport(model, realm);
beatmapUpdater?.Process(model);
}
private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm)
{
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineID > 0).Select(b => b.OnlineID).ToList();
@ -286,64 +288,11 @@ namespace osu.Game.Beatmaps
MD5Hash = memoryStream.ComputeMD5Hash(),
};
updateBeatmapStatistics(beatmap, decoded);
beatmaps.Add(beatmap);
}
}
return beatmaps;
}
private void updateBeatmapStatistics(BeatmapInfo beatmap, IBeatmap decoded)
{
var rulesetInstance = ((IRulesetInfo)beatmap.Ruleset).CreateInstance();
decoded.BeatmapInfo.Ruleset = rulesetInstance.RulesetInfo;
// TODO: this should be done in a better place once we actually need to dynamically update it.
beatmap.StarRating = rulesetInstance.CreateDifficultyCalculator(new DummyConversionBeatmap(decoded)).Calculate().StarRating;
beatmap.Length = calculateLength(decoded);
beatmap.BPM = 60000 / decoded.GetMostCommonBeatLength();
}
private double calculateLength(IBeatmap b)
{
if (!b.HitObjects.Any())
return 0;
var lastObject = b.HitObjects.Last();
//TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list).
double endTime = lastObject.GetEndTime();
double startTime = b.HitObjects.First().StartTime;
return endTime - startTime;
}
public void Dispose()
{
onlineLookupQueue?.Dispose();
}
/// <summary>
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
/// </summary>
private class DummyConversionBeatmap : WorkingBeatmap
{
private readonly IBeatmap beatmap;
public DummyConversionBeatmap(IBeatmap beatmap)
: base(beatmap.BeatmapInfo, null)
{
this.beatmap = beatmap;
}
protected override IBeatmap GetBeatmap() => beatmap;
protected override Texture? GetBackground() => null;
protected override Track? GetBeatmapTrack() => null;
protected internal override ISkin? GetSkin() => null;
public override Stream? GetStream(string storagePath) => null;
}
}
}

View File

@ -41,10 +41,10 @@ namespace osu.Game.Beatmaps
private readonly BeatmapImporter beatmapImporter;
private readonly WorkingBeatmapCache workingBeatmapCache;
private readonly BeatmapOnlineLookupQueue? onlineBeatmapLookupQueue;
private readonly BeatmapUpdater? beatmapUpdater;
public BeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider? api, AudioManager audioManager, IResourceStore<byte[]> gameResources, GameHost? host = null,
WorkingBeatmap? defaultBeatmap = null, bool performOnlineLookups = false)
WorkingBeatmap? defaultBeatmap = null, BeatmapDifficultyCache? difficultyCache = null, bool performOnlineLookups = false)
: base(storage, realm)
{
if (performOnlineLookups)
@ -52,14 +52,17 @@ namespace osu.Game.Beatmaps
if (api == null)
throw new ArgumentNullException(nameof(api), "API must be provided if online lookups are required.");
onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
if (difficultyCache == null)
throw new ArgumentNullException(nameof(difficultyCache), "Difficulty cache must be provided if online lookups are required.");
beatmapUpdater = new BeatmapUpdater(this, difficultyCache, api, storage);
}
var userResources = new RealmFileStore(realm, storage).Store;
BeatmapTrackStore = audioManager.GetTrackStore(userResources);
beatmapImporter = CreateBeatmapImporter(storage, realm, rulesets, onlineBeatmapLookupQueue);
beatmapImporter = CreateBeatmapImporter(storage, realm, rulesets, beatmapUpdater);
beatmapImporter.PostNotification = obj => PostNotification?.Invoke(obj);
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
@ -71,8 +74,8 @@ namespace osu.Game.Beatmaps
return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host);
}
protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue? onlineLookupQueue) =>
new BeatmapImporter(storage, realm, onlineLookupQueue);
protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapUpdater? beatmapUpdater) =>
new BeatmapImporter(storage, realm, beatmapUpdater);
/// <summary>
/// Create a new beatmap set, backed by a <see cref="BeatmapSetInfo"/> model,
@ -314,10 +317,17 @@ namespace osu.Game.Beatmaps
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
Realm.Write(r => setInfo.CopyChangesToRealm(r.Find<BeatmapSetInfo>(setInfo.ID)));
Realm.Write(r =>
{
var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID);
setInfo.CopyChangesToRealm(liveBeatmapSet);
beatmapUpdater?.Process(liveBeatmapSet, r);
});
}
workingBeatmapCache.Invalidate(beatmapInfo);
Debug.Assert(beatmapInfo.BeatmapSet != null);
static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo)
{
@ -418,29 +428,42 @@ namespace osu.Game.Beatmaps
#region Implementation of IWorkingBeatmapCache
public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo? beatmapInfo)
/// <summary>
/// Retrieve a <see cref="WorkingBeatmap"/> instance for the provided <see cref="BeatmapInfo"/>
/// </summary>
/// <param name="beatmapInfo">The beatmap to lookup.</param>
/// <param name="refetch">Whether to force a refetch from the database to ensure <see cref="BeatmapInfo"/> is up-to-date.</param>
/// <returns>A <see cref="WorkingBeatmap"/> instance correlating to the provided <see cref="BeatmapInfo"/>.</returns>
public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo? beatmapInfo, bool refetch = false)
{
if (beatmapInfo != null)
{
// Detached sets don't come with files.
// If we seem to be missing files, now is a good time to re-fetch.
if (beatmapInfo?.IsManaged == true || beatmapInfo?.BeatmapSet?.Files.Count == 0)
if (refetch || beatmapInfo.IsManaged || beatmapInfo.BeatmapSet?.Files.Count == 0)
{
Realm.Run(r =>
{
var refetch = r.Find<BeatmapInfo>(beatmapInfo.ID)?.Detach();
workingBeatmapCache.Invalidate(beatmapInfo);
if (refetch != null)
beatmapInfo = refetch;
});
Guid id = beatmapInfo.ID;
beatmapInfo = Realm.Run(r => r.Find<BeatmapInfo>(id)?.Detach()) ?? beatmapInfo;
}
Debug.Assert(beatmapInfo?.IsManaged != true);
Debug.Assert(beatmapInfo.IsManaged != true);
}
return workingBeatmapCache.GetWorkingBeatmap(beatmapInfo);
}
WorkingBeatmap IWorkingBeatmapCache.GetWorkingBeatmap(BeatmapInfo beatmapInfo) => GetWorkingBeatmap(beatmapInfo);
void IWorkingBeatmapCache.Invalidate(BeatmapSetInfo beatmapSetInfo) => workingBeatmapCache.Invalidate(beatmapSetInfo);
void IWorkingBeatmapCache.Invalidate(BeatmapInfo beatmapInfo) => workingBeatmapCache.Invalidate(beatmapInfo);
public event Action<WorkingBeatmap>? OnInvalidated
{
add => workingBeatmapCache.OnInvalidated += value;
remove => workingBeatmapCache.OnInvalidated -= value;
}
public override bool IsAvailableLocally(BeatmapSetInfo model) => Realm.Run(realm => realm.All<BeatmapSetInfo>().Any(s => s.OnlineID == model.OnlineID));
#endregion
@ -449,7 +472,7 @@ namespace osu.Game.Beatmaps
public void Dispose()
{
onlineBeatmapLookupQueue?.Dispose();
beatmapUpdater?.Dispose();
}
#endregion

View File

@ -0,0 +1,100 @@
// 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.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Rulesets.Objects;
using Realms;
namespace osu.Game.Beatmaps
{
/// <summary>
/// Handles all processing required to ensure a local beatmap is in a consistent state with any changes.
/// </summary>
public class BeatmapUpdater : IDisposable
{
private readonly IWorkingBeatmapCache workingBeatmapCache;
private readonly BeatmapOnlineLookupQueue onlineLookupQueue;
private readonly BeatmapDifficultyCache difficultyCache;
public BeatmapUpdater(IWorkingBeatmapCache workingBeatmapCache, BeatmapDifficultyCache difficultyCache, IAPIProvider api, Storage storage)
{
this.workingBeatmapCache = workingBeatmapCache;
this.difficultyCache = difficultyCache;
onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
}
/// <summary>
/// Queue a beatmap for background processing.
/// </summary>
public void Queue(Live<BeatmapSetInfo> beatmap)
{
// For now, just fire off a task.
// TODO: Add actual queueing probably.
Task.Factory.StartNew(() => beatmap.PerformRead(Process));
}
/// <summary>
/// Run all processing on a beatmap immediately.
/// </summary>
public void Process(BeatmapSetInfo beatmapSet) => beatmapSet.Realm.Write(r => Process(beatmapSet, r));
public void Process(BeatmapSetInfo beatmapSet, Realm realm)
{
// Before we use below, we want to invalidate.
workingBeatmapCache.Invalidate(beatmapSet);
onlineLookupQueue.Update(beatmapSet);
foreach (var beatmap in beatmapSet.Beatmaps)
{
difficultyCache.Invalidate(beatmap);
var working = workingBeatmapCache.GetWorkingBeatmap(beatmap);
var ruleset = working.BeatmapInfo.Ruleset.CreateInstance();
Debug.Assert(ruleset != null);
var calculator = ruleset.CreateDifficultyCalculator(working);
beatmap.StarRating = calculator.Calculate().StarRating;
beatmap.Length = calculateLength(working.Beatmap);
beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength();
}
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
workingBeatmapCache.Invalidate(beatmapSet);
}
private double calculateLength(IBeatmap b)
{
if (!b.HitObjects.Any())
return 0;
var lastObject = b.HitObjects.Last();
//TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list).
double endTime = lastObject.GetEndTime();
double startTime = b.HitObjects.First().StartTime;
return endTime - startTime;
}
#region Implementation of IDisposable
public void Dispose()
{
if (onlineLookupQueue.IsNotNull())
onlineLookupQueue.Dispose();
}
#endregion
}
}

View File

@ -1,8 +1,6 @@
// 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 disable
namespace osu.Game.Beatmaps
{
public interface IWorkingBeatmapCache

View File

@ -76,10 +76,13 @@ namespace osu.Game.Beatmaps
{
Logger.Log($"Invalidating working beatmap cache for {info}");
workingCache.Remove(working);
OnInvalidated?.Invoke(working);
}
}
}
public event Action<WorkingBeatmap> OnInvalidated;
public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo)
{
if (beatmapInfo?.BeatmapSet == null)

View File

@ -3,6 +3,7 @@
#nullable disable
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
@ -39,6 +40,19 @@ namespace osu.Game.Database
return computed;
}
/// <summary>
/// Invalidate all entries matching a provided predicate.
/// </summary>
/// <param name="matchKeyPredicate">The predicate to decide which keys should be invalidated.</param>
protected void Invalidate(Func<TLookup, bool> matchKeyPredicate)
{
foreach (var kvp in cache)
{
if (matchKeyPredicate(kvp.Key))
cache.TryRemove(kvp.Key, out _);
}
}
protected bool CheckExists([NotNull] TLookup lookup, out TValue value) =>
cache.TryGetValue(lookup, out value);

View File

@ -272,7 +272,7 @@ namespace osu.Game
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, difficultyCache, LocalConfig));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true));
dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API));
dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API));

View File

@ -657,9 +657,7 @@ namespace osu.Game.Screens.Edit
// To update the game-wide beatmap with any changes, perform a re-fetch on exit/suspend.
// This is required as the editor makes its local changes via EditorBeatmap
// (which are not propagated outwards to a potentially cached WorkingBeatmap).
((IWorkingBeatmapCache)beatmapManager).Invalidate(Beatmap.Value.BeatmapInfo);
var refetchedBeatmapInfo = beatmapManager.QueryBeatmap(b => b.ID == Beatmap.Value.BeatmapInfo.ID);
var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(refetchedBeatmapInfo);
var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo, true);
if (!(refetchedBeatmap is DummyWorkingBeatmap))
{