Skip to content

Commit 0d6b45e

Browse files
authored
Merge pull request #19431 from peppy/collections-track-beatmap-updates
Add collection transfer logic to beatmap import-as-update flow
2 parents aa03df9 + a59d7f6 commit 0d6b45e

File tree

3 files changed

+162
-3
lines changed

3 files changed

+162
-3
lines changed

osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs

+121
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using NUnit.Framework;
1010
using osu.Framework.Allocation;
1111
using osu.Game.Beatmaps;
12+
using osu.Game.Collections;
1213
using osu.Game.Database;
1314
using osu.Game.Models;
1415
using osu.Game.Overlays.Notifications;
@@ -432,6 +433,126 @@ public void TestMetadataTransferred()
432433
});
433434
}
434435

436+
/// <summary>
437+
/// If all difficulties in the original beatmap set are in a collection, presume the user also wants new difficulties added.
438+
/// </summary>
439+
[TestCase(false)]
440+
[TestCase(true)]
441+
public void TestCollectionTransferNewBeatmap(bool allOriginalBeatmapsInCollection)
442+
{
443+
RunTestWithRealmAsync(async (realm, storage) =>
444+
{
445+
var importer = new BeatmapImporter(storage, realm);
446+
using var rulesets = new RealmRulesetStore(realm, storage);
447+
448+
using var __ = getBeatmapArchive(out string pathOriginal);
449+
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
450+
{
451+
// remove one difficulty before first import
452+
directory.GetFiles("*.osu").First().Delete();
453+
});
454+
455+
var importBeforeUpdate = await importer.Import(new ImportTask(pathMissingOneBeatmap));
456+
457+
Assert.That(importBeforeUpdate, Is.Not.Null);
458+
Debug.Assert(importBeforeUpdate != null);
459+
460+
int beatmapsToAddToCollection = 0;
461+
462+
importBeforeUpdate.PerformWrite(s =>
463+
{
464+
var beatmapCollection = s.Realm.Add(new BeatmapCollection("test collection"));
465+
beatmapsToAddToCollection = s.Beatmaps.Count - (allOriginalBeatmapsInCollection ? 0 : 1);
466+
467+
for (int i = 0; i < beatmapsToAddToCollection; i++)
468+
beatmapCollection.BeatmapMD5Hashes.Add(s.Beatmaps[i].MD5Hash);
469+
});
470+
471+
// Second import matches first but contains one extra .osu file.
472+
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginal), importBeforeUpdate.Value);
473+
474+
Assert.That(importAfterUpdate, Is.Not.Null);
475+
Debug.Assert(importAfterUpdate != null);
476+
477+
importAfterUpdate.PerformRead(updated =>
478+
{
479+
updated.Realm.Refresh();
480+
481+
string[] hashes = updated.Realm.All<BeatmapCollection>().Single().BeatmapMD5Hashes.ToArray();
482+
483+
if (allOriginalBeatmapsInCollection)
484+
{
485+
Assert.That(updated.Beatmaps.Count, Is.EqualTo(beatmapsToAddToCollection + 1));
486+
Assert.That(hashes, Has.Length.EqualTo(updated.Beatmaps.Count));
487+
}
488+
else
489+
{
490+
// Collection contains one less than the original beatmap, and two less after update (new difficulty included).
491+
Assert.That(updated.Beatmaps.Count, Is.EqualTo(beatmapsToAddToCollection + 2));
492+
Assert.That(hashes, Has.Length.EqualTo(beatmapsToAddToCollection));
493+
}
494+
});
495+
});
496+
}
497+
498+
/// <summary>
499+
/// If a difficulty in the original beatmap set is modified, the updated version should remain in any collections it was in.
500+
/// </summary>
501+
[Test]
502+
public void TestCollectionTransferModifiedBeatmap()
503+
{
504+
RunTestWithRealmAsync(async (realm, storage) =>
505+
{
506+
var importer = new BeatmapImporter(storage, realm);
507+
using var rulesets = new RealmRulesetStore(realm, storage);
508+
509+
using var __ = getBeatmapArchive(out string pathOriginal);
510+
using var _ = getBeatmapArchiveWithModifications(out string pathModified, directory =>
511+
{
512+
// Modify one .osu file with different content.
513+
var firstOsuFile = directory.GetFiles("*[Hard]*.osu").First();
514+
515+
string existingContent = File.ReadAllText(firstOsuFile.FullName);
516+
517+
File.WriteAllText(firstOsuFile.FullName, existingContent + "\n# I am new content");
518+
});
519+
520+
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
521+
522+
Assert.That(importBeforeUpdate, Is.Not.Null);
523+
Debug.Assert(importBeforeUpdate != null);
524+
525+
string originalHash = string.Empty;
526+
527+
importBeforeUpdate.PerformWrite(s =>
528+
{
529+
var beatmapCollection = s.Realm.Add(new BeatmapCollection("test collection"));
530+
originalHash = s.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash;
531+
532+
beatmapCollection.BeatmapMD5Hashes.Add(originalHash);
533+
});
534+
535+
// Second import matches first but contains a modified .osu file.
536+
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathModified), importBeforeUpdate.Value);
537+
538+
Assert.That(importAfterUpdate, Is.Not.Null);
539+
Debug.Assert(importAfterUpdate != null);
540+
541+
importAfterUpdate.PerformRead(updated =>
542+
{
543+
updated.Realm.Refresh();
544+
545+
string[] hashes = updated.Realm.All<BeatmapCollection>().Single().BeatmapMD5Hashes.ToArray();
546+
string updatedHash = updated.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash;
547+
548+
Assert.That(hashes, Has.Length.EqualTo(1));
549+
Assert.That(hashes.First(), Is.EqualTo(updatedHash));
550+
551+
Assert.That(updatedHash, Is.Not.EqualTo(originalHash));
552+
});
553+
});
554+
}
555+
435556
private static void checkCount<T>(RealmAccess realm, int expected, Expression<Func<T, bool>>? condition = null) where T : RealmObject
436557
{
437558
var query = realm.Realm.All<T>();

osu.Game/Beatmaps/BeatmapImporter.cs

+37
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using osu.Framework.Platform;
1515
using osu.Framework.Testing;
1616
using osu.Game.Beatmaps.Formats;
17+
using osu.Game.Collections;
1718
using osu.Game.Database;
1819
using osu.Game.Extensions;
1920
using osu.Game.IO;
@@ -71,6 +72,8 @@ public BeatmapImporter(Storage storage, RealmAccess realm)
7172
// Transfer local values which should be persisted across a beatmap update.
7273
updated.DateAdded = original.DateAdded;
7374

75+
transferCollectionReferences(realm, original, updated);
76+
7477
foreach (var beatmap in original.Beatmaps.ToArray())
7578
{
7679
var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.Hash == beatmap.Hash);
@@ -112,6 +115,40 @@ public BeatmapImporter(Storage storage, RealmAccess realm)
112115
return first;
113116
}
114117

118+
private static void transferCollectionReferences(Realm realm, BeatmapSetInfo original, BeatmapSetInfo updated)
119+
{
120+
// First check if every beatmap in the original set is in any collections.
121+
// In this case, we will assume they also want any newly added difficulties added to the collection.
122+
foreach (var c in realm.All<BeatmapCollection>())
123+
{
124+
if (original.Beatmaps.Select(b => b.MD5Hash).All(c.BeatmapMD5Hashes.Contains))
125+
{
126+
foreach (var b in original.Beatmaps)
127+
c.BeatmapMD5Hashes.Remove(b.MD5Hash);
128+
129+
foreach (var b in updated.Beatmaps)
130+
c.BeatmapMD5Hashes.Add(b.MD5Hash);
131+
}
132+
}
133+
134+
// Handle collections using permissive difficulty name to track difficulties.
135+
foreach (var originalBeatmap in original.Beatmaps)
136+
{
137+
var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalBeatmap.DifficultyName);
138+
139+
if (updatedBeatmap == null)
140+
continue;
141+
142+
var collections = realm.All<BeatmapCollection>().AsEnumerable().Where(c => c.BeatmapMD5Hashes.Contains(originalBeatmap.MD5Hash));
143+
144+
foreach (var c in collections)
145+
{
146+
c.BeatmapMD5Hashes.Remove(originalBeatmap.MD5Hash);
147+
c.BeatmapMD5Hashes.Add(updatedBeatmap.MD5Hash);
148+
}
149+
}
150+
}
151+
115152
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz";
116153

117154
protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)

osu.Game/Database/Live.cs

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// See the LICENCE file in the repository root for full licence text.
33

44
using System;
5+
using JetBrains.Annotations;
56

67
namespace osu.Game.Database
78
{
@@ -18,19 +19,19 @@ public abstract class Live<T> : IEquatable<Live<T>>
1819
/// Perform a read operation on this live object.
1920
/// </summary>
2021
/// <param name="perform">The action to perform.</param>
21-
public abstract void PerformRead(Action<T> perform);
22+
public abstract void PerformRead([InstantHandle] Action<T> perform);
2223

2324
/// <summary>
2425
/// Perform a read operation on this live object.
2526
/// </summary>
2627
/// <param name="perform">The action to perform.</param>
27-
public abstract TReturn PerformRead<TReturn>(Func<T, TReturn> perform);
28+
public abstract TReturn PerformRead<TReturn>([InstantHandle] Func<T, TReturn> perform);
2829

2930
/// <summary>
3031
/// Perform a write operation on this live object.
3132
/// </summary>
3233
/// <param name="perform">The action to perform.</param>
33-
public abstract void PerformWrite(Action<T> perform);
34+
public abstract void PerformWrite([InstantHandle] Action<T> perform);
3435

3536
/// <summary>
3637
/// Whether this instance is tracking data which is managed by the database backing.

0 commit comments

Comments
 (0)