1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-26 08:10:35 +08:00

Merge pull request #36516 from peppy/fix-beatmap-invalidation-F-A-I-L

Fix star ratings sometimes not updating after editing a beatmap
This commit is contained in:
Bartłomiej Dach
2026-01-29 08:31:56 +01:00
committed by GitHub
Unverified
4 changed files with 94 additions and 12 deletions
@@ -4,6 +4,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -15,6 +16,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual;
@@ -34,6 +36,12 @@ namespace osu.Game.Tests.Beatmaps
private IBindable<StarDifficulty> starDifficultyBindable;
[Resolved]
private BeatmapManager beatmapManager { get; set; }
[Resolved]
private BeatmapDifficultyCache actualDifficultyCache { get; set; }
[BackgroundDependencyLoader]
private void load(OsuGameBase osu)
{
@@ -55,6 +63,36 @@ namespace osu.Game.Tests.Beatmaps
AddUntilStep($"star difficulty -> {BASE_STARS}", () => starDifficultyBindable.Value.Stars == BASE_STARS);
}
[Test]
public void TestInvalidationFlow()
{
BeatmapInfo postEditBeatmapInfo = null;
BeatmapInfo preEditBeatmapInfo = null;
IBindable<StarDifficulty> bindableDifficulty = null;
AddStep("get bindable stars", () =>
{
preEditBeatmapInfo = importedSet.Beatmaps.First();
bindableDifficulty = actualDifficultyCache.GetBindableDifficulty(preEditBeatmapInfo);
});
AddUntilStep("wait for stars retrieved", () => bindableDifficulty.Value.Stars, () => Is.GreaterThan(0));
AddStep("remove all hitobjects", () =>
{
var working = beatmapManager.GetWorkingBeatmap(preEditBeatmapInfo);
((IList<HitObject>)working.Beatmap.HitObjects).Clear();
beatmapManager.Save(working.BeatmapInfo, working.Beatmap);
postEditBeatmapInfo = working.BeatmapInfo;
});
AddAssert("stars is now zero", () => actualDifficultyCache.GetDifficultyAsync(postEditBeatmapInfo).GetResultSafely()!.Value.Stars, () => Is.Zero);
AddUntilStep("bindable stars is now zero", () => bindableDifficulty.Value.Stars, () => Is.Zero);
}
[Test]
public void TestStarDifficultyChangesOnModSettings()
{
@@ -122,8 +160,10 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestKeyDoesntEqualWithDifferentModSettings()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.9 } } });
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 },
new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 },
new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.9 } } });
Assert.That(key1, Is.Not.EqualTo(key2));
Assert.That(key1.GetHashCode(), Is.Not.EqualTo(key2.GetHashCode()));
@@ -132,8 +172,10 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestKeyEqualWithMatchingModSettings()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 },
new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 },
new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
Assert.That(key1, Is.EqualTo(key2));
Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
+34 -5
View File
@@ -75,6 +75,10 @@ namespace osu.Game.Beatmaps
currentMods.BindValueChanged(mods =>
{
// A change in bindable here doesn't guarantee that mods have actually changed.
if (mods.OldValue.SequenceEqual(mods.NewValue))
return;
modSettingChangeTracker?.Dispose();
Scheduler.AddOnce(updateTrackedBindables);
@@ -82,15 +86,37 @@ namespace osu.Game.Beatmaps
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
modSettingChangeTracker.SettingChanged += _ =>
{
debouncedModSettingsChange?.Cancel();
debouncedModSettingsChange = Scheduler.AddDelayed(updateTrackedBindables, 100);
lock (bindableUpdateLock)
{
debouncedModSettingsChange?.Cancel();
debouncedModSettingsChange = Scheduler.AddDelayed(updateTrackedBindables, 100);
}
};
}, true);
}
public void Invalidate(IBeatmapInfo beatmap)
/// <summary>
/// Notify this cache that a beatmap has been invalidated/updated.
/// </summary>
/// <param name="oldBeatmap">The old beatmap model.</param>
/// <param name="newBeatmap">The updated beatmap model.</param>
public void Invalidate(IBeatmapInfo oldBeatmap, IBeatmapInfo newBeatmap)
{
base.Invalidate(lookup => lookup.BeatmapInfo.Equals(beatmap));
base.Invalidate(lookup => lookup.BeatmapInfo.Equals(oldBeatmap));
lock (bindableUpdateLock)
{
bool trackedBindablesRefreshRequired = false;
foreach (var bsd in trackedBindables.Where(bsd => bsd.BeatmapInfo.Equals(oldBeatmap)))
{
bsd.BeatmapInfo = newBeatmap;
trackedBindablesRefreshRequired = true;
}
if (trackedBindablesRefreshRequired)
Scheduler.AddOnce(updateTrackedBindables);
}
}
/// <summary>
@@ -195,6 +221,9 @@ namespace osu.Game.Beatmaps
{
lock (bindableUpdateLock)
{
debouncedModSettingsChange?.Cancel();
debouncedModSettingsChange = null;
trackedUpdateCancellationSource.Cancel();
trackedUpdateCancellationSource = new CancellationTokenSource();
@@ -348,7 +377,7 @@ namespace osu.Game.Beatmaps
private class BindableStarDifficulty : Bindable<StarDifficulty>
{
public readonly IBeatmapInfo BeatmapInfo;
public IBeatmapInfo BeatmapInfo;
public readonly CancellationToken CancellationToken;
public BindableStarDifficulty(IBeatmapInfo beatmapInfo, CancellationToken cancellationToken)
+3 -3
View File
@@ -52,11 +52,11 @@ namespace osu.Game.Beatmaps
foreach (BeatmapInfo beatmap in beatmapSet.Beatmaps)
{
difficultyCache.Invalidate(beatmap);
var working = workingBeatmapCache.GetWorkingBeatmap(beatmap);
var ruleset = working.BeatmapInfo.Ruleset.CreateInstance();
difficultyCache.Invalidate(beatmap, working.BeatmapInfo);
var ruleset = working.BeatmapInfo.Ruleset.CreateInstance();
var calculator = ruleset.CreateDifficultyCalculator(working);
beatmap.StarRating = calculator.Calculate().StarRating;
+11
View File
@@ -4,6 +4,7 @@
#nullable disable
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using JetBrains.Annotations;
@@ -104,6 +105,10 @@ namespace osu.Game.Beatmaps
beatmapInfo = beatmapInfo.Detach();
// If this ever gets hit, a request has arrived with an outdated BeatmapInfo.
// An outdated BeatmapInfo may contain a reference to a previous version of the beatmap's files on disk.
Debug.Assert(confirmFileHashIsUpToDate(beatmapInfo), "working beatmap returned with outdated path");
workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this));
// best effort; may be higher than expected.
@@ -113,6 +118,12 @@ namespace osu.Game.Beatmaps
}
}
private bool confirmFileHashIsUpToDate(BeatmapInfo beatmapInfo)
{
string refetchPath = realm.Run(r => r.Find<BeatmapInfo>(beatmapInfo.ID)?.File?.File.Hash);
return refetchPath == null || refetchPath == beatmapInfo.File?.File.Hash;
}
#region IResourceStorageProvider
TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore;