mirror of https://github.com/ppy/osu
Merge pull request #24169 from bdach/legacy-score-v2
Backpopulate stable ScoreV2 scores with ScoreV2 system mod (and don't recalculate their total score)
This commit is contained in:
commit
8e1e8a2807
|
@ -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))]
|
||||
|
|
|
@ -91,6 +91,9 @@ public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
|
|||
|
||||
if (mods.HasFlagFast(LegacyMods.Relax))
|
||||
yield return new CatchModRelax();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.ScoreV2))
|
||||
yield return new ModScoreV2();
|
||||
}
|
||||
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type)
|
||||
|
@ -140,6 +143,12 @@ public override IEnumerable<Mod> GetModsFor(ModType type)
|
|||
new CatchModNoScope(),
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
return new Mod[]
|
||||
{
|
||||
new ModScoreV2(),
|
||||
};
|
||||
|
||||
default:
|
||||
return Array.Empty<Mod>();
|
||||
}
|
||||
|
|
|
@ -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))]
|
||||
|
|
|
@ -157,6 +157,9 @@ public override IEnumerable<Mod> 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<Mod> GetModsFor(ModType type)
|
|||
new ModAdaptiveSpeed()
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
return new Mod[]
|
||||
{
|
||||
new ModScoreV2(),
|
||||
};
|
||||
|
||||
default:
|
||||
return Array.Empty<Mod>();
|
||||
}
|
||||
|
|
|
@ -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))]
|
||||
|
|
|
@ -113,6 +113,9 @@ public override IEnumerable<Mod> 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<Mod> GetModsFor(ModType type)
|
|||
return new Mod[]
|
||||
{
|
||||
new OsuModTouchDevice(),
|
||||
new ModScoreV2(),
|
||||
};
|
||||
|
||||
default:
|
||||
|
|
|
@ -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))]
|
||||
|
|
|
@ -116,6 +116,9 @@ public override IEnumerable<Mod> 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<Mod> GetModsFor(ModType type)
|
|||
new ModAdaptiveSpeed()
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
return new Mod[]
|
||||
{
|
||||
new ModScoreV2(),
|
||||
};
|
||||
|
||||
default:
|
||||
return Array.Empty<Mod>();
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ public enum LegacyMods
|
|||
Key1 = 1 << 26,
|
||||
Key3 = 1 << 27,
|
||||
Key2 = 1 << 28,
|
||||
ScoreV2 = 1 << 29,
|
||||
Mirror = 1 << 30,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
/// </summary>
|
||||
private const int schema_version = 31;
|
||||
private const int schema_version = 32;
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held during <see cref="BlockAllOperations"/> 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<T>() where T : RealmObject
|
|||
|
||||
case 28:
|
||||
{
|
||||
var files = new RealmFileStore(this, storage);
|
||||
var scores = migration.NewRealm.All<ScoreInfo>();
|
||||
|
||||
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<T>() where T : RealmObject
|
|||
|
||||
break;
|
||||
}
|
||||
|
||||
case 32:
|
||||
{
|
||||
foreach (var score in migration.NewRealm.All<ScoreInfo>())
|
||||
{
|
||||
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");
|
||||
|
|
|
@ -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
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to populate the <paramref name="score"/> model using data parsed from its corresponding replay file.
|
||||
/// </summary>
|
||||
/// <param name="score">The score to run population from replay for.</param>
|
||||
/// <param name="files">A <see cref="RealmFileStore"/> instance to use for fetching replay.</param>
|
||||
/// <param name="populationFunc">
|
||||
/// Delegate describing the population to execute.
|
||||
/// The delegate's argument is a <see cref="SerializationReader"/> instance which permits to read data from the replay stream.
|
||||
/// </param>
|
||||
public static void PopulateFromReplay(this ScoreInfo score, RealmFileStore files, Action<SerializationReader> 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;
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
|
||||
{
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -192,6 +192,10 @@ public virtual LegacyMods ConvertToLegacyMods(Mod[] mods)
|
|||
case ModAutoplay:
|
||||
value |= LegacyMods.Autoplay;
|
||||
break;
|
||||
|
||||
case ModScoreV2:
|
||||
value |= LegacyMods.ScoreV2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue