From 655d725f0c16620e66ee046510e1d8d137abd9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Feb 2026 05:34:43 +0100 Subject: [PATCH] Perform extra checks when loading rulesets (#36641) --- osu.Game.Tests/Database/RulesetStoreTests.cs | 71 ++++++++++++++++++++ osu.Game/Rulesets/RealmRulesetStore.cs | 6 ++ 2 files changed, 77 insertions(+) diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index 29aec73770..7ef2429491 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -7,6 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -116,6 +117,69 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestFakedRulesetIdIsDetected() + { + RunTestWithRealm((realm, storage) => + { + LoadTestRuleset.HasImplementations = true; + LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION; + + var ruleset = new LoadTestRuleset(); + string rulesetShortName = ruleset.RulesetInfo.ShortName; + + realm.Write(r => r.Add(new RulesetInfo(rulesetShortName, ruleset.RulesetInfo.Name, ruleset.RulesetInfo.InstantiationInfo, 0) + { + Available = true, + })); + + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True); + + // Availability is updated on construction of a RealmRulesetStore + using var _ = new RealmRulesetStore(realm, storage); + + Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.False); + }); + } + + [Test] + public void TestMultipleRulesetWithSameOnlineIdsAreDetected() + { + RunTestWithRealm((realm, storage) => + { + LoadTestRuleset.HasImplementations = true; + LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION; + LoadTestRuleset.OnlineID = 2; + + var first = new LoadTestRuleset(); + var second = new CatchRuleset(); + + realm.Write(r => r.Add(new RulesetInfo(first.ShortName, first.RulesetInfo.Name, first.RulesetInfo.InstantiationInfo, first.RulesetInfo.OnlineID) + { + Available = true, + })); + realm.Write(r => r.Add(new RulesetInfo(second.ShortName, second.RulesetInfo.Name, second.RulesetInfo.InstantiationInfo, second.RulesetInfo.OnlineID) + { + Available = true, + })); + + Assert.That(realm.Run(r => r.Find(first.ShortName)!.Available), Is.True); + Assert.That(realm.Run(r => r.Find(second.ShortName)!.Available), Is.True); + + // Availability is updated on construction of a RealmRulesetStore + using var _ = new RealmRulesetStore(realm, storage); + + Assert.That(realm.Run(r => r.Find(first.ShortName)!.Available), Is.False); + Assert.That(realm.Run(r => r.Find(second.ShortName)!.Available), Is.False); + + realm.Write(r => r.Remove(r.Find(first.ShortName)!)); + + using var __ = new RealmRulesetStore(realm, storage); + + Assert.That(realm.Run(r => r.Find(second.ShortName)!.Available), Is.True); + }); + } + private class LoadTestRuleset : Ruleset { public override string RulesetAPIVersionSupported => Version; @@ -124,6 +188,13 @@ namespace osu.Game.Tests.Database public static string Version { get; set; } = CURRENT_RULESET_API_VERSION; + public static int OnlineID { get; set; } = -1; + + public LoadTestRuleset() + { + RulesetInfo.OnlineID = OnlineID; + } + public override IEnumerable GetModsFor(ModType type) { if (!HasImplementations) diff --git a/osu.Game/Rulesets/RealmRulesetStore.cs b/osu.Game/Rulesets/RealmRulesetStore.cs index b93110426b..52ea5f7f4b 100644 --- a/osu.Game/Rulesets/RealmRulesetStore.cs +++ b/osu.Game/Rulesets/RealmRulesetStore.cs @@ -93,6 +93,12 @@ namespace osu.Game.Rulesets $"Ruleset API version is too old (was {instance.RulesetAPIVersionSupported}, expected {Ruleset.CURRENT_RULESET_API_VERSION})"); } + if (r.OnlineID != instanceInfo.OnlineID) + throw new InvalidOperationException($@"Online ID mismatch for ruleset {r.ShortName}: database has {r.OnlineID}, constructed instance has {instanceInfo.OnlineID}"); + + if (r.OnlineID > 0 && rulesets.Any(otherRuleset => otherRuleset.ShortName != r.ShortName && otherRuleset.OnlineID == r.OnlineID)) + throw new InvalidOperationException($@"Ruleset {r.ShortName} shares online ID {r.OnlineID} with another ruleset"); + // If a ruleset isn't up-to-date with the API, it could cause a crash at an arbitrary point of execution. // To eagerly handle cases of missing implementations, enumerate all types here and mark as non-available on throw. resolvedType.Assembly.GetTypes();