Skip to content

Commit 89fe51a

Browse files
Add versioned playable prewarm and persisted slider ticks metadata
Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/013bac5c-f284-486b-a01e-396d84a127ce Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
1 parent a31eadc commit 89fe51a

14 files changed

Lines changed: 233 additions & 31 deletions

File tree

osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -142,27 +142,23 @@ private void load(OsuRulesetConfigManager? config, IBeatmap? beatmap)
142142

143143
// handle edge cases where a beatmap has a slider with many repeats.
144144
int maxRepeatsOnOneSlider;
145-
int maxTicksOnOneSlider = 0;
145+
int maxTicksOnOneSlider;
146146

147147
// Use persisted metadata when available to avoid a full beatmap scan.
148148
// Fall back to scanning the loaded beatmap if metadata has not yet been computed.
149149
int persistedMaxRepeats = beatmap?.BeatmapInfo.MaxSliderRepeats ?? -1;
150+
int persistedMaxTicks = beatmap?.BeatmapInfo.MaxSliderTicks ?? -1;
150151

151-
if (persistedMaxRepeats >= 0)
152+
if (persistedMaxRepeats >= 0 && persistedMaxTicks >= 0)
152153
{
153154
maxRepeatsOnOneSlider = persistedMaxRepeats;
154-
155-
// Still scan for tick counts since MaxSliderTicks is not yet persisted.
156-
if (osuBeatmap != null)
157-
{
158-
foreach (var slider in osuBeatmap.HitObjects.OfType<Slider>())
159-
maxTicksOnOneSlider = Math.Max(maxTicksOnOneSlider, slider.NestedHitObjects.OfType<SliderTick>().Count());
160-
}
155+
maxTicksOnOneSlider = persistedMaxTicks;
161156
}
162157
else
163158
{
164159
// Fallback: scan all sliders when persisted metadata is not available.
165160
maxRepeatsOnOneSlider = 0;
161+
maxTicksOnOneSlider = 0;
166162

167163
if (osuBeatmap != null)
168164
{
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System.Linq;
5+
using System.Numerics;
6+
using System.Threading;
7+
using NUnit.Framework;
8+
using osu.Game.Beatmaps;
9+
using osu.Game.Rulesets.Objects;
10+
using osu.Game.Rulesets.Osu.Beatmaps;
11+
using osu.Game.Rulesets.Osu.Objects;
12+
13+
namespace osu.Game.Tests.Beatmaps
14+
{
15+
[TestFixture]
16+
public class BeatmapInfoStartupMetadataTest
17+
{
18+
[Test]
19+
public void TestSliderStartupMetadataCalculated()
20+
{
21+
var beatmap = new OsuBeatmap
22+
{
23+
HitObjects =
24+
{
25+
new Slider
26+
{
27+
StartTime = 0,
28+
RepeatCount = 2,
29+
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(200, 0) }),
30+
},
31+
new Slider
32+
{
33+
StartTime = 1500,
34+
RepeatCount = 4,
35+
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(400, 0) }),
36+
},
37+
}
38+
};
39+
40+
foreach (var hitObject in beatmap.HitObjects)
41+
hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty, CancellationToken.None);
42+
43+
var info = new BeatmapInfo();
44+
info.UpdateStatisticsFromBeatmap(beatmap);
45+
46+
Assert.That(info.MaxSliderRepeats, Is.EqualTo(4));
47+
Assert.That(info.MaxSliderTicks, Is.GreaterThan(0));
48+
Assert.That(beatmap.HitObjects.OfType<Slider>().Any(s => s.NestedHitObjects.OfType<SliderTick>().Any()), Is.True);
49+
}
50+
}
51+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System;
5+
using NUnit.Framework;
6+
using osu.Game.Beatmaps;
7+
using osu.Game.Rulesets.Mods;
8+
9+
namespace osu.Game.Tests.Beatmaps
10+
{
11+
[TestFixture]
12+
public class PlayableBeatmapCacheTest
13+
{
14+
[Test]
15+
public void TestReturnsClone()
16+
{
17+
var cache = new PlayableBeatmapCache();
18+
var beatmap = new Beatmap
19+
{
20+
BeatmapInfo = new BeatmapInfo
21+
{
22+
Hash = "hash"
23+
}
24+
};
25+
26+
cache.CachePlayableBeatmap(beatmap.BeatmapInfo, beatmap.BeatmapInfo.Ruleset, Array.Empty<Mod>(), beatmap);
27+
28+
Assert.That(cache.TryGetPlayableBeatmap(beatmap.BeatmapInfo, beatmap.BeatmapInfo.Ruleset, Array.Empty<Mod>(), out var retrieved), Is.True);
29+
Assert.That(retrieved, Is.Not.SameAs(beatmap));
30+
}
31+
32+
[Test]
33+
public void TestHashChangeMissesCache()
34+
{
35+
var cache = new PlayableBeatmapCache();
36+
var beatmapInfo = new BeatmapInfo
37+
{
38+
Hash = "hash-a"
39+
};
40+
41+
var beatmap = new Beatmap { BeatmapInfo = beatmapInfo };
42+
43+
cache.CachePlayableBeatmap(beatmapInfo, beatmapInfo.Ruleset, Array.Empty<Mod>(), beatmap);
44+
Assert.That(cache.TryGetPlayableBeatmap(beatmapInfo, beatmapInfo.Ruleset, Array.Empty<Mod>(), out _), Is.True);
45+
46+
beatmapInfo.Hash = "hash-b";
47+
Assert.That(cache.TryGetPlayableBeatmap(beatmapInfo, beatmapInfo.Ruleset, Array.Empty<Mod>(), out _), Is.False);
48+
}
49+
}
50+
}

osu.Game.Tournament/Models/TournamentBeatmap.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public class TournamentBeatmap : IBeatmapInfo, IBeatmapSetOnlineInfo
2828

2929
public int MaxSliderRepeats { get; set; }
3030

31+
public int MaxSliderTicks { get; set; }
32+
3133
public IBeatmapMetadataInfo Metadata { get; set; } = new BeatmapMetadata();
3234

3335
public IBeatmapDifficultyInfo Difficulty { get; set; } = new BeatmapDifficulty();
@@ -52,6 +54,8 @@ public TournamentBeatmap(APIBeatmap beatmap)
5254
Covers = beatmap.BeatmapSet?.Covers ?? new BeatmapSetOnlineCovers();
5355
EndTimeObjectCount = beatmap.EndTimeObjectCount;
5456
TotalObjectCount = beatmap.TotalObjectCount;
57+
MaxSliderRepeats = beatmap.MaxSliderRepeats;
58+
MaxSliderTicks = beatmap.MaxSliderTicks;
5559
Ruleset = beatmap.Ruleset;
5660
}
5761

osu.Game/Beatmaps/BeatmapInfo.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ public BeatmapOnlineStatus Status
129129
/// </summary>
130130
public int MaxSliderRepeats { get; set; } = -1;
131131

132+
/// <summary>
133+
/// The maximum number of slider ticks found on a single slider in the beatmap.
134+
/// Only relevant for rulesets that expose slider ticks (currently osu!).
135+
/// Defaults to -1 (meaning not-yet-calculated).
136+
/// </summary>
137+
public int MaxSliderTicks { get; set; } = -1;
138+
132139
/// <summary>
133140
/// Reset any fetched online linking information (and history).
134141
/// </summary>

osu.Game/Beatmaps/BeatmapInfoExtensions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ public static void UpdateStatisticsFromBeatmap(this BeatmapInfo beatmapInfo, IBe
3030
.Select(h => h.RepeatCount)
3131
.DefaultIfEmpty(0)
3232
.Max();
33+
34+
// Slider tick objects are generated as nested hit objects after defaults have been applied.
35+
// This method is called both with raw and playable beatmaps; for raw beatmaps this will
36+
// naturally become 0 and later be backfilled by background processing.
37+
beatmapInfo.MaxSliderTicks = beatmap.HitObjects
38+
.Select(h => h.NestedHitObjects.Count(n => n.GetType().Name == "SliderTick"))
39+
.DefaultIfEmpty(0)
40+
.Max();
3341
}
3442

3543
/// <summary>

osu.Game/Beatmaps/BeatmapUpdater.cs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
22
// See the LICENCE file in the repository root for full licence text.
33

4-
using System.Linq;
54
using System.Threading;
65
using System.Threading.Tasks;
76
using osu.Framework.Extensions.ObjectExtensions;
@@ -10,7 +9,6 @@
109
using osu.Framework.Threading;
1110
using osu.Game.Database;
1211
using osu.Game.Online.API;
13-
using osu.Game.Rulesets.Objects.Types;
1412

1513
namespace osu.Game.Beatmaps
1614
{
@@ -59,9 +57,10 @@ public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope =
5957

6058
var ruleset = working.BeatmapInfo.Ruleset.CreateInstance();
6159
var calculator = ruleset.CreateDifficultyCalculator(working);
60+
var playable = working.GetPlayableBeatmap(working.BeatmapInfo.Ruleset);
6261

6362
beatmap.StarRating = calculator.Calculate().StarRating;
64-
beatmap.UpdateStatisticsFromBeatmap(working.Beatmap);
63+
beatmap.UpdateStatisticsFromBeatmap(playable);
6564
}
6665

6766
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
@@ -77,15 +76,9 @@ public void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope loo
7776
workingBeatmapCache.Invalidate(beatmapInfo);
7877

7978
var working = workingBeatmapCache.GetWorkingBeatmap(beatmapInfo);
80-
var beatmap = working.Beatmap;
81-
82-
beatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration);
83-
beatmapInfo.TotalObjectCount = beatmap.HitObjects.Count;
84-
beatmapInfo.MaxSliderRepeats = beatmap.HitObjects
85-
.OfType<IHasRepeats>()
86-
.Select(h => h.RepeatCount)
87-
.DefaultIfEmpty(0)
88-
.Max();
79+
var playable = working.GetPlayableBeatmap(beatmapInfo.Ruleset);
80+
81+
beatmapInfo.UpdateStatisticsFromBeatmap(playable);
8982

9083
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
9184
workingBeatmapCache.Invalidate(beatmapInfo);

osu.Game/Beatmaps/IBeatmapInfo.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,12 @@ public interface IBeatmapInfo : IHasOnlineID<int>, IEquatable<IBeatmapInfo>
8484
/// Defaults to -1 (meaning not-yet-calculated).
8585
/// </summary>
8686
int MaxSliderRepeats { get; }
87+
88+
/// <summary>
89+
/// The maximum number of slider ticks on a single slider in the beatmap.
90+
/// Only meaningful for rulesets that have slider ticks (e.g. osu!).
91+
/// Defaults to -1 (meaning not-yet-calculated).
92+
/// </summary>
93+
int MaxSliderTicks { get; }
8794
}
8895
}

osu.Game/Beatmaps/PlayableBeatmapCache.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using osu.Framework.Allocation;
99
using osu.Framework.Graphics;
1010
using osu.Framework.Logging;
11+
using osu.Game;
1112
using osu.Game.Rulesets;
1213
using osu.Game.Rulesets.Mods;
1314

@@ -21,7 +22,7 @@ namespace osu.Game.Beatmaps
2122
/// <remarks>
2223
/// <para>
2324
/// The cache stores one <em>canonical</em> (immutable) copy keyed by
24-
/// <c>(BeatmapInfo.ID, rulesetShortName, orderedModsKey)</c>.
25+
/// <c>(BeatmapInfo.ID + BeatmapInfo.Hash, rulesetShortName, orderedModsKey, gameVersion)</c>.
2526
/// Callers receive a shallow <see cref="IBeatmap.Clone"/> so that gameplay state does not
2627
/// bleed across sessions while still reusing the pre-built hit-object graph.
2728
/// </para>
@@ -33,20 +34,23 @@ namespace osu.Game.Beatmaps
3334
/// </remarks>
3435
public class PlayableBeatmapCache : Component
3536
{
36-
private readonly record struct CacheKey(Guid BeatmapId, string RulesetShortName, string ModsKey);
37+
private readonly record struct CacheKey(Guid BeatmapId, string BeatmapHash, string RulesetShortName, string ModsKey, string GameVersion);
3738

3839
private readonly Dictionary<CacheKey, IBeatmap> cache = new Dictionary<CacheKey, IBeatmap>();
3940

4041
private WorkingBeatmapCache? workingBeatmapCache;
42+
private string gameVersion = string.Empty;
4143

42-
[BackgroundDependencyLoader]
43-
private void load(IWorkingBeatmapCache beatmapCache)
44+
[BackgroundDependencyLoader(true)]
45+
private void load(IWorkingBeatmapCache beatmapCache, OsuGameBase? game)
4446
{
4547
if (beatmapCache is WorkingBeatmapCache concrete)
4648
{
4749
workingBeatmapCache = concrete;
4850
workingBeatmapCache.OnInvalidated += handleInvalidated;
4951
}
52+
53+
gameVersion = game?.VersionHash ?? typeof(OsuGameBase).Assembly.GetName().Version?.ToString() ?? "unknown";
5054
}
5155

5256
/// <summary>
@@ -110,15 +114,15 @@ private void handleInvalidated(WorkingBeatmap working)
110114
}
111115
}
112116

113-
private static CacheKey makeKey(BeatmapInfo beatmapInfo, IRulesetInfo ruleset, IReadOnlyList<Mod> mods)
117+
private CacheKey makeKey(BeatmapInfo beatmapInfo, IRulesetInfo ruleset, IReadOnlyList<Mod> mods)
114118
{
115119
// Build a deterministic key from ordered mod acronyms and their settings hash.
116120
// Mod.GetHashCode() accounts for both the type and any user-adjustable settings.
117121
string modsKey = string.Join(';', mods
118122
.OrderBy(m => m.Acronym)
119123
.Select(m => $"{m.Acronym}:{m.GetHashCode()}"));
120124

121-
return new CacheKey(beatmapInfo.ID, ruleset.ShortName, modsKey);
125+
return new CacheKey(beatmapInfo.ID, beatmapInfo.Hash, ruleset.ShortName, modsKey, gameVersion);
122126
}
123127

124128
protected override void Dispose(bool isDisposing)

osu.Game/Database/BackgroundDataStoreProcessor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ private void processBeatmapsWithMissingObjectCounts()
295295

296296
realmAccess.Run(r =>
297297
{
298-
foreach (var b in r.All<BeatmapInfo>().Where(b => b.TotalObjectCount < 0 || b.EndTimeObjectCount < 0 || b.MaxSliderRepeats < 0))
298+
foreach (var b in r.All<BeatmapInfo>().Where(b => b.TotalObjectCount < 0 || b.EndTimeObjectCount < 0 || b.MaxSliderRepeats < 0 || b.MaxSliderTicks < 0))
299299
beatmapIds.Add(b.ID);
300300
});
301301

0 commit comments

Comments
 (0)