diff --git a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs index b74120fa3c..dacfd649ef 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Mods; using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Catch.Tests @@ -24,7 +25,8 @@ public class CatchLegacyModConversionTest : LegacyModConversionTest new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } }, new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } }, new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } }, - new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } } + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } }, + new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, }; [TestCaseSource(nameof(catch_mod_mapping))] diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 8f1a1b8ef5..e51e5cc5db 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -91,6 +91,9 @@ public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) if (mods.HasFlagFast(LegacyMods.Relax)) yield return new CatchModRelax(); + + if (mods.HasFlagFast(LegacyMods.ScoreV2)) + yield return new ModScoreV2(); } public override IEnumerable GetModsFor(ModType type) @@ -140,6 +143,12 @@ public override IEnumerable GetModsFor(ModType type) new CatchModNoScope(), }; + case ModType.System: + return new Mod[] + { + new ModScoreV2(), + }; + default: return Array.Empty(); } diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs index 3a9639e04d..cb2abc1595 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mods; using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Mania.Tests @@ -36,7 +37,8 @@ public class ManiaLegacyModConversionTest : LegacyModConversionTest new object[] { LegacyMods.Key3, new[] { typeof(ManiaModKey3) } }, new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } }, new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } }, - new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } } + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } }, + new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, }; [TestCaseSource(nameof(mania_mod_mapping))] diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 2e96c89516..bd6ab4086b 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -157,6 +157,9 @@ public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) if (mods.HasFlagFast(LegacyMods.Mirror)) yield return new ManiaModMirror(); + + if (mods.HasFlagFast(LegacyMods.ScoreV2)) + yield return new ModScoreV2(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -285,6 +288,12 @@ public override IEnumerable GetModsFor(ModType type) new ModAdaptiveSpeed() }; + case ModType.System: + return new Mod[] + { + new ModScoreV2(), + }; + default: return Array.Empty(); } diff --git a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs index 05366e9444..2cf9842c83 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs @@ -4,6 +4,7 @@ using System; using NUnit.Framework; using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Tests.Beatmaps; @@ -28,7 +29,8 @@ public class OsuLegacyModConversionTest : LegacyModConversionTest new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } }, new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } }, new object[] { LegacyMods.Target, new[] { typeof(OsuModTargetPractice) } }, - new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } } + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } }, + new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, }; [TestCaseSource(nameof(osu_mod_mapping))] diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index b44d999d4f..036d13c5aa 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -113,6 +113,9 @@ public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) if (mods.HasFlagFast(LegacyMods.TouchDevice)) yield return new OsuModTouchDevice(); + + if (mods.HasFlagFast(LegacyMods.ScoreV2)) + yield return new ModScoreV2(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -212,6 +215,7 @@ public override IEnumerable GetModsFor(ModType type) return new Mod[] { new OsuModTouchDevice(), + new ModScoreV2(), }; default: diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs index 541987d63e..5f7a78ddf1 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs @@ -4,6 +4,7 @@ using System; using NUnit.Framework; using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Tests.Beatmaps; @@ -25,7 +26,8 @@ public class TaikoLegacyModConversionTest : LegacyModConversionTest new object[] { LegacyMods.Flashlight, new[] { typeof(TaikoModFlashlight) } }, new object[] { LegacyMods.Autoplay, new[] { typeof(TaikoModAutoplay) } }, new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } }, - new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } } + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } }, + new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, }; [TestCaseSource(nameof(taiko_mod_mapping))] diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index aa31b1924f..de3fa1750f 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -116,6 +116,9 @@ public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) if (mods.HasFlagFast(LegacyMods.Random)) yield return new TaikoModRandom(); + + if (mods.HasFlagFast(LegacyMods.ScoreV2)) + yield return new ModScoreV2(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -176,6 +179,12 @@ public override IEnumerable GetModsFor(ModType type) new ModAdaptiveSpeed() }; + case ModType.System: + return new Mod[] + { + new ModScoreV2(), + }; + default: return Array.Empty(); } diff --git a/osu.Game/Beatmaps/Legacy/LegacyMods.cs b/osu.Game/Beatmaps/Legacy/LegacyMods.cs index 0e517ea3df..747015d90a 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyMods.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyMods.cs @@ -38,6 +38,7 @@ public enum LegacyMods Key1 = 1 << 26, Key3 = 1 << 27, Key2 = 1 << 28, + ScoreV2 = 1 << 29, Mirror = 1 << 30, } } diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index f9f11c49ff..f32b161bb6 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -15,17 +15,19 @@ using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Extensions; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Input.Bindings; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; using osu.Framework.Threading; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Input.Bindings; -using osu.Game.IO.Legacy; using osu.Game.Models; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -79,8 +81,9 @@ public class RealmAccess : IDisposable /// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes. /// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations. /// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores. + /// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files. /// - private const int schema_version = 31; + private const int schema_version = 32; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -730,6 +733,8 @@ private void applyMigrationsForVersion(Migration migration, ulong targetVersion) Logger.Log($"Running realm migration to version {targetVersion}..."); Stopwatch stopwatch = new Stopwatch(); + var files = new RealmFileStore(this, storage); + stopwatch.Start(); switch (targetVersion) @@ -904,36 +909,17 @@ void convertOnlineIDs() where T : RealmObject case 28: { - var files = new RealmFileStore(this, storage); var scores = migration.NewRealm.All(); foreach (var score in scores) { - string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath(); - if (replayFilename == null) - continue; - - try + score.PopulateFromReplay(files, sr => { - using (var stream = files.Store.GetStream(replayFilename)) - { - if (stream == null) - continue; - - // Trimmed down logic from LegacyScoreDecoder to extract the version from replays. - using (SerializationReader sr = new SerializationReader(stream)) - { - sr.ReadByte(); // Ruleset. - int version = sr.ReadInt32(); - if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION) - score.IsLegacyScore = true; - } - } - } - catch (Exception e) - { - Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database); - } + sr.ReadByte(); // Ruleset. + int version = sr.ReadInt32(); + if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION) + score.IsLegacyScore = true; + }); } break; @@ -986,6 +972,46 @@ void convertOnlineIDs() where T : RealmObject break; } + + case 32: + { + foreach (var score in migration.NewRealm.All()) + { + if (!score.IsLegacyScore || !score.Ruleset.IsLegacyRuleset()) + continue; + + score.PopulateFromReplay(files, sr => + { + sr.ReadByte(); // Ruleset. + sr.ReadInt32(); // Version. + sr.ReadString(); // Beatmap hash. + sr.ReadString(); // Username. + sr.ReadString(); // MD5Hash. + sr.ReadUInt16(); // Count300. + sr.ReadUInt16(); // Count100. + sr.ReadUInt16(); // Count50. + sr.ReadUInt16(); // CountGeki. + sr.ReadUInt16(); // CountKatu. + sr.ReadUInt16(); // CountMiss. + + // we should have this in LegacyTotalScore already, but if we're reading through this anyways... + int totalScore = sr.ReadInt32(); + + sr.ReadUInt16(); // Max combo. + sr.ReadBoolean(); // Perfect. + + var legacyMods = (LegacyMods)sr.ReadInt32(); + + if (!legacyMods.HasFlagFast(LegacyMods.ScoreV2) || score.APIMods.Any(mod => mod.Acronym == @"SV2")) + return; + + score.APIMods = score.APIMods.Append(new APIMod(new ModScoreV2())).ToArray(); + score.LegacyTotalScore = score.TotalScore = totalScore; + }); + } + + break; + } } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 60530c31cb..b8afdad294 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -5,10 +5,14 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Framework.Logging; using osu.Game.Beatmaps; +using osu.Game.Extensions; +using osu.Game.IO.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -205,6 +209,10 @@ public static long ConvertFromLegacyTotalScore(ScoreInfo score, BeatmapManager b if (ruleset is not ILegacyRuleset legacyRuleset) return score.TotalScore; + var mods = score.Mods; + if (mods.Any(mod => mod is ModScoreV2)) + return score.TotalScore; + var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods); if (playableBeatmap.HitObjects.Count == 0) @@ -212,7 +220,7 @@ public static long ConvertFromLegacyTotalScore(ScoreInfo score, BeatmapManager b ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator(); - sv1Simulator.Simulate(beatmap, playableBeatmap, score.Mods); + sv1Simulator.Simulate(beatmap, playableBeatmap, mods); return ConvertFromLegacyTotalScore(score, new DifficultyAttributes { @@ -282,6 +290,38 @@ public static long ConvertFromLegacyTotalScore(ScoreInfo score, DifficultyAttrib } } + /// + /// Used to populate the model using data parsed from its corresponding replay file. + /// + /// The score to run population from replay for. + /// A instance to use for fetching replay. + /// + /// Delegate describing the population to execute. + /// The delegate's argument is a instance which permits to read data from the replay stream. + /// + public static void PopulateFromReplay(this ScoreInfo score, RealmFileStore files, Action populationFunc) + { + string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath(); + if (replayFilename == null) + return; + + try + { + using (var stream = files.Store.GetStream(replayFilename)) + { + if (stream == null) + return; + + using (SerializationReader sr = new SerializationReader(stream)) + populationFunc.Invoke(sr); + } + } + catch (Exception e) + { + Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database); + } + } + private class FakeHit : HitObject { private readonly Judgement judgement; diff --git a/osu.Game/Rulesets/Mods/ModScoreV2.cs b/osu.Game/Rulesets/Mods/ModScoreV2.cs new file mode 100644 index 0000000000..6d56b2d86f --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModScoreV2.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// This mod is used strictly to mark osu!stable scores set with the "Score V2" mod active. + /// It should not be used in any real capacity going forward. + /// + public class ModScoreV2 : Mod + { + public override string Name => "Score V2"; + public override string Acronym => @"SV2"; + public override ModType Type => ModType.System; + public override LocalisableString Description => "Score set on earlier osu! versions with the V2 scoring algorithm active."; + public override double ScoreMultiplier => 1; + } +} diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 490ec1475c..cd432e050b 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -192,6 +192,10 @@ public virtual LegacyMods ConvertToLegacyMods(Mod[] mods) case ModAutoplay: value |= LegacyMods.Autoplay; break; + + case ModScoreV2: + value |= LegacyMods.ScoreV2; + break; } }