1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 16:12:57 +08:00

Merge pull request #19378 from peppy/beatmap-update-test

Add separate beatmap update flow to handle edge cases better
This commit is contained in:
Dan Balasescu 2022-07-26 18:49:20 +09:00 committed by GitHub
commit 8f7dff5c2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 626 additions and 14 deletions

View File

@ -670,6 +670,61 @@ namespace osu.Game.Tests.Database
});
}
[Test]
public void TestImportThenReimportWithNewDifficulty()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? pathOriginal = TestResources.GetTestBeatmapForImport();
string pathMissingOneBeatmap = pathOriginal.Replace(".osz", "_missing_difficulty.osz");
string extractedFolder = $"{pathOriginal}_extracted";
Directory.CreateDirectory(extractedFolder);
try
{
using (var zip = ZipArchive.Open(pathOriginal))
zip.WriteToDirectory(extractedFolder);
// remove one difficulty before first import
new FileInfo(Directory.GetFiles(extractedFolder, "*.osu").First()).Delete();
using (var zip = ZipArchive.Create())
{
zip.AddAllFromDirectory(extractedFolder);
zip.SaveTo(pathMissingOneBeatmap, new ZipWriterOptions(CompressionType.Deflate));
}
var firstImport = await importer.Import(new ImportTask(pathMissingOneBeatmap));
Assert.That(firstImport, Is.Not.Null);
Assert.That(realm.Realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending), Has.Count.EqualTo(1));
Assert.That(realm.Realm.All<BeatmapSetInfo>().First(s => !s.DeletePending).Beatmaps, Has.Count.EqualTo(11));
// Second import matches first but contains one extra .osu file.
var secondImport = await importer.Import(new ImportTask(pathOriginal));
Assert.That(secondImport, Is.Not.Null);
Assert.That(realm.Realm.All<BeatmapInfo>(), Has.Count.EqualTo(23));
Assert.That(realm.Realm.All<BeatmapSetInfo>(), Has.Count.EqualTo(2));
Assert.That(realm.Realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending), Has.Count.EqualTo(1));
Assert.That(realm.Realm.All<BeatmapSetInfo>().First(s => !s.DeletePending).Beatmaps, Has.Count.EqualTo(12));
// check the newly "imported" beatmap is not the original.
Assert.That(firstImport?.ID, Is.Not.EqualTo(secondImport?.ID));
}
finally
{
Directory.Delete(extractedFolder, true);
}
});
}
[Test]
public void TestImportThenReimportAfterMissingFiles()
{
@ -742,7 +797,7 @@ namespace osu.Game.Tests.Database
await realm.Realm.WriteAsync(() =>
{
foreach (var b in imported.Beatmaps)
b.OnlineID = -1;
b.ResetOnlineInfo();
});
deleteBeatmapSet(imported, realm.Realm);

View File

@ -0,0 +1,447 @@
// 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.IO;
using System.Linq;
using System.Linq.Expressions;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Models;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Tests.Resources;
using Realms;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Writers.Zip;
namespace osu.Game.Tests.Database
{
/// <summary>
/// Tests the flow where a beatmap is already loaded and an update is applied.
/// </summary>
[TestFixture]
public class BeatmapImporterUpdateTests : RealmTest
{
private const int count_beatmaps = 12;
[Test]
public void TestNewDifficultyAdded()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
{
// remove one difficulty before first import
directory.GetFiles("*.osu").First().Delete();
});
var importBeforeUpdate = await importer.Import(new ImportTask(pathMissingOneBeatmap));
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
checkCount<BeatmapSetInfo>(realm, 1, s => !s.DeletePending);
Assert.That(importBeforeUpdate.Value.Beatmaps, Has.Count.EqualTo(count_beatmaps - 1));
// Second import matches first but contains one extra .osu file.
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginal), importBeforeUpdate.Value);
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
checkCount<BeatmapInfo>(realm, count_beatmaps);
checkCount<BeatmapMetadata>(realm, count_beatmaps);
checkCount<BeatmapSetInfo>(realm, 1);
// check the newly "imported" beatmap is not the original.
Assert.That(importBeforeUpdate.ID, Is.Not.EqualTo(importAfterUpdate.ID));
// Previous beatmap set has no beatmaps so will be completely purged on the spot.
Assert.That(importBeforeUpdate.Value.IsValid, Is.False);
});
}
/// <summary>
/// Regression test covering https://github.com/ppy/osu/issues/19369 (import potentially duplicating if original has no <see cref="BeatmapInfo.OnlineID"/>).
/// </summary>
[Test]
public void TestNewDifficultyAddedNoOnlineID()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
{
// remove one difficulty before first import
directory.GetFiles("*.osu").First().Delete();
});
var importBeforeUpdate = await importer.Import(new ImportTask(pathMissingOneBeatmap));
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
// This test is the same as TestNewDifficultyAdded except for this block.
importBeforeUpdate.PerformWrite(s =>
{
s.OnlineID = -1;
foreach (var beatmap in s.Beatmaps)
beatmap.ResetOnlineInfo();
});
checkCount<BeatmapSetInfo>(realm, 1, s => !s.DeletePending);
Assert.That(importBeforeUpdate.Value.Beatmaps, Has.Count.EqualTo(count_beatmaps - 1));
// Second import matches first but contains one extra .osu file.
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginal), importBeforeUpdate.Value);
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
checkCount<BeatmapInfo>(realm, count_beatmaps);
checkCount<BeatmapMetadata>(realm, count_beatmaps);
checkCount<BeatmapSetInfo>(realm, 1);
// check the newly "imported" beatmap is not the original.
Assert.That(importBeforeUpdate.ID, Is.Not.EqualTo(importAfterUpdate.ID));
// Previous beatmap set has no beatmaps so will be completely purged on the spot.
Assert.That(importBeforeUpdate.Value.IsValid, Is.False);
});
}
[Test]
public void TestExistingDifficultyModified()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchiveWithModifications(out string pathModified, directory =>
{
// Modify one .osu file with different content.
var firstOsuFile = directory.GetFiles("*.osu").First();
string existingContent = File.ReadAllText(firstOsuFile.FullName);
File.WriteAllText(firstOsuFile.FullName, existingContent + "\n# I am new content");
});
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
checkCount<BeatmapSetInfo>(realm, 1, s => !s.DeletePending);
Assert.That(importBeforeUpdate.Value.Beatmaps, Has.Count.EqualTo(count_beatmaps));
// Second import matches first but contains one extra .osu file.
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathModified), importBeforeUpdate.Value);
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
// should only contain the modified beatmap (others purged).
Assert.That(importBeforeUpdate.Value.Beatmaps, Has.Count.EqualTo(1));
Assert.That(importAfterUpdate.Value.Beatmaps, Has.Count.EqualTo(count_beatmaps));
checkCount<BeatmapInfo>(realm, count_beatmaps + 1);
checkCount<BeatmapMetadata>(realm, count_beatmaps + 1);
checkCount<BeatmapSetInfo>(realm, 1, s => !s.DeletePending);
checkCount<BeatmapSetInfo>(realm, 1, s => s.DeletePending);
});
}
[Test]
public void TestExistingDifficultyRemoved()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
{
// remove one difficulty before first import
directory.GetFiles("*.osu").First().Delete();
});
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
Assert.That(importBeforeUpdate.Value.Beatmaps, Has.Count.EqualTo(count_beatmaps));
Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1));
// Second import matches first but contains one extra .osu file.
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathMissingOneBeatmap), importBeforeUpdate.Value);
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
checkCount<BeatmapInfo>(realm, count_beatmaps);
checkCount<BeatmapMetadata>(realm, count_beatmaps);
checkCount<BeatmapSetInfo>(realm, 2);
// previous set should contain the removed beatmap still.
Assert.That(importBeforeUpdate.Value.Beatmaps, Has.Count.EqualTo(1));
Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.EqualTo(-1));
// Previous beatmap set has no beatmaps so will be completely purged on the spot.
Assert.That(importAfterUpdate.Value.Beatmaps, Has.Count.EqualTo(count_beatmaps - 1));
});
}
[Test]
public void TestUpdatedImportContainsNothing()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchiveWithModifications(out string pathEmpty, directory =>
{
foreach (var file in directory.GetFiles())
file.Delete();
});
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathEmpty), importBeforeUpdate.Value);
Assert.That(importAfterUpdate, Is.Null);
checkCount<BeatmapSetInfo>(realm, 1);
checkCount<BeatmapInfo>(realm, count_beatmaps);
checkCount<BeatmapMetadata>(realm, count_beatmaps);
Assert.That(importBeforeUpdate.Value.IsValid, Is.True);
});
}
[Test]
public void TestNoChanges()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchive(out string pathOriginalSecond);
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value);
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
checkCount<BeatmapSetInfo>(realm, 1);
checkCount<BeatmapInfo>(realm, count_beatmaps);
checkCount<BeatmapMetadata>(realm, count_beatmaps);
Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1));
Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID));
});
}
[Test]
public void TestScoreTransferredOnUnchanged()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
{
// arbitrary beatmap removal
directory.GetFiles("*.osu").First().Delete();
});
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
string scoreTargetBeatmapHash = string.Empty;
importBeforeUpdate.PerformWrite(s =>
{
var beatmapInfo = s.Beatmaps.Last();
scoreTargetBeatmapHash = beatmapInfo.Hash;
s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
});
checkCount<ScoreInfo>(realm, 1);
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathMissingOneBeatmap), importBeforeUpdate.Value);
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
checkCount<BeatmapInfo>(realm, count_beatmaps);
checkCount<BeatmapMetadata>(realm, count_beatmaps);
checkCount<BeatmapSetInfo>(realm, 2);
// score is transferred across to the new set
checkCount<ScoreInfo>(realm, 1);
Assert.That(importAfterUpdate.Value.Beatmaps.First(b => b.Hash == scoreTargetBeatmapHash).Scores, Has.Count.EqualTo(1));
});
}
[Test]
public void TestScoreLostOnModification()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
string? scoreTargetFilename = string.Empty;
importBeforeUpdate.PerformWrite(s =>
{
var beatmapInfo = s.Beatmaps.Last();
scoreTargetFilename = beatmapInfo.File?.Filename;
s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
});
checkCount<ScoreInfo>(realm, 1);
using var _ = getBeatmapArchiveWithModifications(out string pathModified, directory =>
{
// Modify one .osu file with different content.
var firstOsuFile = directory.GetFiles(scoreTargetFilename).First();
string existingContent = File.ReadAllText(firstOsuFile.FullName);
File.WriteAllText(firstOsuFile.FullName, existingContent + "\n# I am new content");
});
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathModified), importBeforeUpdate.Value);
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
checkCount<BeatmapInfo>(realm, count_beatmaps + 1);
checkCount<BeatmapMetadata>(realm, count_beatmaps + 1);
checkCount<BeatmapSetInfo>(realm, 2);
// score is not transferred due to modifications.
checkCount<ScoreInfo>(realm, 1);
Assert.That(importBeforeUpdate.Value.Beatmaps.AsEnumerable().First(b => b.File?.Filename == scoreTargetFilename).Scores, Has.Count.EqualTo(1));
Assert.That(importAfterUpdate.Value.Beatmaps.AsEnumerable().First(b => b.File?.Filename == scoreTargetFilename).Scores, Has.Count.EqualTo(0));
});
}
[Test]
public void TestMetadataTransferred()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
{
// arbitrary beatmap removal
directory.GetFiles("*.osu").First().Delete();
});
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathMissingOneBeatmap), importBeforeUpdate.Value);
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
Assert.That(importBeforeUpdate.ID, Is.Not.EqualTo(importAfterUpdate.ID));
Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(importAfterUpdate.Value.DateAdded));
});
}
private static void checkCount<T>(RealmAccess realm, int expected, Expression<Func<T, bool>>? condition = null) where T : RealmObject
{
var query = realm.Realm.All<T>();
if (condition != null)
query = query.Where(condition);
Assert.That(query, Has.Count.EqualTo(expected));
}
private static IDisposable getBeatmapArchiveWithModifications(out string path, Action<DirectoryInfo> applyModifications)
{
var cleanup = getBeatmapArchive(out path);
string extractedFolder = $"{path}_extracted";
Directory.CreateDirectory(extractedFolder);
using (var zip = ZipArchive.Open(path))
zip.WriteToDirectory(extractedFolder);
applyModifications(new DirectoryInfo(extractedFolder));
File.Delete(path);
using (var zip = ZipArchive.Create())
{
zip.AddAllFromDirectory(extractedFolder);
zip.SaveTo(path, new ZipWriterOptions(CompressionType.Deflate));
}
Directory.Delete(extractedFolder, true);
return cleanup;
}
private static IDisposable getBeatmapArchive(out string path, bool quick = true)
{
string beatmapPath = TestResources.GetTestBeatmapForImport(quick);
path = beatmapPath;
return new InvokeOnDisposal(() => File.Delete(beatmapPath));
}
}
}

View File

@ -239,7 +239,7 @@ namespace osu.Game.Tests.Visual.Gameplay
createPlayerTest(false, r =>
{
var beatmap = createTestBeatmap(r);
beatmap.BeatmapInfo.OnlineID = -1;
beatmap.BeatmapInfo.ResetOnlineInfo();
return beatmap;
});

View File

@ -3,9 +3,11 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Logging;
@ -16,6 +18,7 @@ using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using Realms;
@ -38,6 +41,77 @@ namespace osu.Game.Beatmaps
{
}
public override async Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original)
{
var imported = await Import(notification, importTask);
if (!imported.Any())
return null;
Debug.Assert(imported.Count() == 1);
var first = imported.First();
// If there were no changes, ensure we don't accidentally nuke ourselves.
if (first.ID == original.ID)
return first;
first.PerformWrite(updated =>
{
var realm = updated.Realm;
Logger.Log($"Beatmap \"{updated}\" update completed successfully", LoggingTarget.Database);
original = realm.Find<BeatmapSetInfo>(original.ID);
// Generally the import process will do this for us if the OnlineIDs match,
// but that isn't a guarantee (ie. if the .osu file doesn't have OnlineIDs populated).
original.DeletePending = true;
// Transfer local values which should be persisted across a beatmap update.
updated.DateAdded = original.DateAdded;
foreach (var beatmap in original.Beatmaps.ToArray())
{
var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.Hash == beatmap.Hash);
if (updatedBeatmap != null)
{
// If the updated beatmap matches an existing one, transfer any user data across..
if (beatmap.Scores.Any())
{
Logger.Log($"Transferring {beatmap.Scores.Count()} scores for unchanged difficulty \"{beatmap}\"", LoggingTarget.Database);
foreach (var score in beatmap.Scores)
score.BeatmapInfo = updatedBeatmap;
}
// ..then nuke the old beatmap completely.
// this is done instead of a soft deletion to avoid a user potentially creating weird
// interactions, like restoring the outdated beatmap then updating a second time
// (causing user data to be wiped).
original.Beatmaps.Remove(beatmap);
realm.Remove(beatmap.Metadata);
realm.Remove(beatmap);
}
else
{
// If the beatmap differs in the original, leave it in a soft-deleted state but reset online info.
// This caters to the case where a user has made modifications they potentially want to restore,
// but after restoring we want to ensure it can't be used to trigger an update of the beatmap.
beatmap.ResetOnlineInfo();
}
}
// If the original has no beatmaps left, delete the set as well.
if (!original.Beatmaps.Any())
realm.Remove(original);
});
return first;
}
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz";
protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)
@ -87,7 +161,7 @@ namespace osu.Game.Beatmaps
existingSetWithSameOnlineID.OnlineID = -1;
foreach (var b in existingSetWithSameOnlineID.Beatmaps)
b.OnlineID = -1;
b.ResetOnlineInfo();
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be disassociated and marked for deletion.");
}
@ -133,7 +207,7 @@ namespace osu.Game.Beatmaps
}
}
void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineID = -1);
void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.ResetOnlineInfo());
}
protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import)

View File

@ -109,6 +109,17 @@ namespace osu.Game.Beatmaps
[JsonIgnore]
public bool Hidden { get; set; }
/// <summary>
/// Reset any fetched online linking information (and history).
/// </summary>
public void ResetOnlineInfo()
{
OnlineID = -1;
LastOnlineUpdate = null;
OnlineMD5Hash = string.Empty;
Status = BeatmapOnlineStatus.None;
}
#region Properties we may not want persisted (but also maybe no harm?)
public double AudioLeadIn { get; set; }

View File

@ -164,8 +164,7 @@ namespace osu.Game.Beatmaps
// clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps.
newBeatmapInfo.Hash = string.Empty;
// clear online properties.
newBeatmapInfo.OnlineID = -1;
newBeatmapInfo.Status = BeatmapOnlineStatus.None;
newBeatmapInfo.ResetOnlineInfo();
return addDifficultyToSet(targetBeatmapSet, newBeatmap, referenceWorkingBeatmap.Skin);
}
@ -409,6 +408,9 @@ namespace osu.Game.Beatmaps
Realm.Run(r => Undelete(r.All<BeatmapSetInfo>().Where(s => s.DeletePending).ToList()));
}
public Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original) =>
beatmapImporter.ImportAsUpdate(notification, importTask, original);
#region Implementation of ICanAcceptFiles
public Task Import(params string[] paths) => beatmapImporter.Import(paths);

View File

@ -88,7 +88,7 @@ namespace osu.Game.Beatmaps
if (req.CompletionState == APIRequestCompletionState.Failed)
{
logForModel(set, $"Online retrieval failed for {beatmapInfo}");
beatmapInfo.OnlineID = -1;
beatmapInfo.ResetOnlineInfo();
return;
}
@ -118,7 +118,7 @@ namespace osu.Game.Beatmaps
catch (Exception e)
{
logForModel(set, $"Online retrieval failed for {beatmapInfo} ({e.Message})");
beatmapInfo.OnlineID = -1;
beatmapInfo.ResetOnlineInfo();
}
}

View File

@ -23,6 +23,16 @@ namespace osu.Game.Database
/// <returns>The imported models.</returns>
Task<IEnumerable<Live<TModel>>> Import(ProgressNotification notification, params ImportTask[] tasks);
/// <summary>
/// Process a single import as an update for an existing model.
/// This will still run a full import, but perform any post-processing required to make it feel like an update to the user.
/// </summary>
/// <param name="notification">The notification to update.</param>
/// <param name="task">The import task.</param>
/// <param name="original">The original model which is being updated.</param>
/// <returns>The imported model.</returns>
Task<Live<TModel>?> ImportAsUpdate(ProgressNotification notification, ImportTask task, TModel original);
/// <summary>
/// A user displayable name for the model type associated with this manager.
/// </summary>

View File

@ -42,7 +42,11 @@ namespace osu.Game.Database
/// <returns>The request object.</returns>
protected abstract ArchiveDownloadRequest<T> CreateDownloadRequest(T model, bool minimiseDownloadSize);
public bool Download(T model, bool minimiseDownloadSize = false)
public bool Download(T model, bool minimiseDownloadSize = false) => Download(model, minimiseDownloadSize, null);
public void DownloadAsUpdate(TModel originalModel) => Download(originalModel, false, originalModel);
protected bool Download(T model, bool minimiseDownloadSize, TModel? originalModel)
{
if (!canDownload(model)) return false;
@ -63,11 +67,15 @@ namespace osu.Game.Database
{
Task.Factory.StartNew(async () =>
{
// This gets scheduled back to the update thread, but we want the import to run in the background.
var imported = await importer.Import(notification, new ImportTask(filename)).ConfigureAwait(false);
bool importSuccessful;
if (originalModel != null)
importSuccessful = (await importer.ImportAsUpdate(notification, new ImportTask(filename), originalModel)) != null;
else
importSuccessful = (await importer.Import(notification, new ImportTask(filename))).Any();
// for now a failed import will be marked as a failed download for simplicity.
if (!imported.Any())
if (!importSuccessful)
DownloadFailed?.Invoke(request);
CurrentDownloads.Remove(request);

View File

@ -278,7 +278,6 @@ namespace osu.Game.Database
realm.Remove(score);
realm.Remove(beatmap.Metadata);
realm.Remove(beatmap);
}

View File

@ -174,6 +174,8 @@ namespace osu.Game.Database
return imported;
}
public virtual Task<Live<TModel>?> ImportAsUpdate(ProgressNotification notification, ImportTask task, TModel original) => throw new NotImplementedException();
/// <summary>
/// Import one <typeparamref name="TModel"/> from the filesystem and delete the file on success.
/// Note that this bypasses the UI flow and should only be used for special cases or testing.

View File

@ -268,6 +268,8 @@ namespace osu.Game.Scoring
public Task<IEnumerable<Live<ScoreInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => scoreImporter.Import(notification, tasks);
public Task<Live<ScoreInfo>> ImportAsUpdate(ProgressNotification notification, ImportTask task, ScoreInfo original) => scoreImporter.ImportAsUpdate(notification, task, original);
public Live<ScoreInfo> Import(ScoreInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default) =>
scoreImporter.ImportModel(item, archive, batchImport, cancellationToken);

View File

@ -90,7 +90,7 @@ namespace osu.Game.Screens.Select.Carousel
Action = () =>
{
beatmapDownloader.Download(beatmapSetInfo);
beatmapDownloader.DownloadAsUpdate(beatmapSetInfo);
attachExistingDownload();
};
}

View File

@ -272,6 +272,8 @@ namespace osu.Game.Skinning
public Task<IEnumerable<Live<SkinInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => skinImporter.Import(notification, tasks);
public Task<Live<SkinInfo>> ImportAsUpdate(ProgressNotification notification, ImportTask task, SkinInfo original) => skinImporter.ImportAsUpdate(notification, task, original);
public Task<Live<SkinInfo>> Import(ImportTask task, bool batchImport = false, CancellationToken cancellationToken = default) => skinImporter.Import(task, batchImport, cancellationToken);
#endregion