2019-01-24 16:43:03 +08:00
|
|
|
|
// 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.
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2022-06-17 15:37:17 +08:00
|
|
|
|
#nullable disable
|
|
|
|
|
|
2018-03-01 00:32:32 +08:00
|
|
|
|
using System;
|
2022-12-11 12:00:12 +08:00
|
|
|
|
using System.Diagnostics;
|
2018-02-28 15:34:47 +08:00
|
|
|
|
using System.IO;
|
2018-11-28 15:12:57 +08:00
|
|
|
|
using System.Linq;
|
2022-12-06 19:10:51 +08:00
|
|
|
|
using Newtonsoft.Json;
|
2018-02-28 15:34:47 +08:00
|
|
|
|
using osu.Game.Beatmaps;
|
2019-04-01 10:23:07 +08:00
|
|
|
|
using osu.Game.Beatmaps.Formats;
|
2018-11-28 15:12:57 +08:00
|
|
|
|
using osu.Game.Beatmaps.Legacy;
|
Fix incorrect accuracy and rank population when decoding lazer replays
Closes https://github.com/ppy/osu/issues/24061.
The gist of this change is that if the `LegacyReplaySoloScoreInfo`
bolt-on is present in the replay, then it can (and is) used to recompute
the accuracy, and rank is computed based on that.
This was the missing part of
https://github.com/ppy/osu/issues/24061#issuecomment-1888438151.
The accuracy would change on import before that because the encode
process is _lossy_ if the `LegacyReplaySoloScoreInfo` bolt-on is not
used, as the legacy format only has 6 fields for encoding judgement
counts, and some judgements that affect accuracy in lazer do not fit
into that.
Note that this _only_ fixes _relatively_ new lazer scores looking wrong
after reimport.
- Very old lazer scores, i.e. ones that don't have the
`LegacyReplaySoloScoreInfo` bolt-on, obviously can't use it
to repopulate. There's really not much good that can be done there,
so the stable pathways are used as a fallback that always works.
- For stable replays, `ScoreImporter` recalculates the accuracy of
the score _again_ in
https://github.com/ppy/osu/blob/15a5fd7e4c8e8e3c38382cfd3d5d676d107d7908/osu.Game/Scoring/ScoreImporter.cs#L106-L110
as `StandardisedScoreMigrationTools.UpdateFromLegacy()` recomputes
_both_ total score and accuracy.
This makes a _semblance_ of sense as it attempts to make the accuracy
of stable and lazer replays comparable. In most cases it also won't
matter, as the only ruleset where accuracy changed between the legacy
implementation and current lazer accuracy is mania.
But it is also an inaccurate process (as, again, some of the required
data is not in the replay, namely judgement counts of ticks
and so on).
For whatever's worth, a similar thing happens server-side in
https://github.com/ppy/osu-queue-score-statistics/blob/106c2948dbe695efcad5972d32cd46f4b36005cc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs#L319
- However, _ranks_ of stable scores will still use the local stable
reimplementation of ranks, i.e. a 1-miss stable score in osu! ruleset
will be an A rather than an S. See importer:
https://github.com/ppy/osu-queue-score-statistics/blob/106c2948dbe695efcad5972d32cd46f4b36005cc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs#L237
(it's the same method which is renamed
to `PopulateLegacyAccuracyAndRank()` in this commit).
That is all a bit of a mess honestly, but I'm not sure where to even
begin there...
2024-01-17 04:08:02 +08:00
|
|
|
|
using osu.Game.Database;
|
2018-02-28 15:34:47 +08:00
|
|
|
|
using osu.Game.IO.Legacy;
|
2021-11-04 17:02:44 +08:00
|
|
|
|
using osu.Game.Online.API.Requests.Responses;
|
2018-11-28 16:20:37 +08:00
|
|
|
|
using osu.Game.Replays;
|
|
|
|
|
using osu.Game.Replays.Legacy;
|
2018-11-28 15:12:57 +08:00
|
|
|
|
using osu.Game.Rulesets;
|
2018-11-30 13:48:19 +08:00
|
|
|
|
using osu.Game.Rulesets.Mods;
|
2018-02-28 15:34:47 +08:00
|
|
|
|
using osu.Game.Rulesets.Replays;
|
|
|
|
|
using SharpCompress.Compressors.LZMA;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-11-28 15:12:57 +08:00
|
|
|
|
namespace osu.Game.Scoring.Legacy
|
2018-02-28 15:34:47 +08:00
|
|
|
|
{
|
2020-03-24 09:38:24 +08:00
|
|
|
|
public abstract class LegacyScoreDecoder
|
2018-02-28 15:34:47 +08:00
|
|
|
|
{
|
2018-04-19 19:44:38 +08:00
|
|
|
|
private IBeatmap currentBeatmap;
|
2018-02-28 15:34:47 +08:00
|
|
|
|
private Ruleset currentRuleset;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2022-03-24 14:49:13 +08:00
|
|
|
|
private float beatmapOffset;
|
|
|
|
|
|
2018-11-28 17:45:17 +08:00
|
|
|
|
public Score Parse(Stream stream)
|
2018-02-28 15:34:47 +08:00
|
|
|
|
{
|
2018-11-28 17:45:17 +08:00
|
|
|
|
var score = new Score
|
|
|
|
|
{
|
|
|
|
|
Replay = new Replay()
|
|
|
|
|
};
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2020-03-31 16:13:42 +08:00
|
|
|
|
WorkingBeatmap workingBeatmap;
|
Fix incorrect accuracy and rank population when decoding lazer replays
Closes https://github.com/ppy/osu/issues/24061.
The gist of this change is that if the `LegacyReplaySoloScoreInfo`
bolt-on is present in the replay, then it can (and is) used to recompute
the accuracy, and rank is computed based on that.
This was the missing part of
https://github.com/ppy/osu/issues/24061#issuecomment-1888438151.
The accuracy would change on import before that because the encode
process is _lossy_ if the `LegacyReplaySoloScoreInfo` bolt-on is not
used, as the legacy format only has 6 fields for encoding judgement
counts, and some judgements that affect accuracy in lazer do not fit
into that.
Note that this _only_ fixes _relatively_ new lazer scores looking wrong
after reimport.
- Very old lazer scores, i.e. ones that don't have the
`LegacyReplaySoloScoreInfo` bolt-on, obviously can't use it
to repopulate. There's really not much good that can be done there,
so the stable pathways are used as a fallback that always works.
- For stable replays, `ScoreImporter` recalculates the accuracy of
the score _again_ in
https://github.com/ppy/osu/blob/15a5fd7e4c8e8e3c38382cfd3d5d676d107d7908/osu.Game/Scoring/ScoreImporter.cs#L106-L110
as `StandardisedScoreMigrationTools.UpdateFromLegacy()` recomputes
_both_ total score and accuracy.
This makes a _semblance_ of sense as it attempts to make the accuracy
of stable and lazer replays comparable. In most cases it also won't
matter, as the only ruleset where accuracy changed between the legacy
implementation and current lazer accuracy is mania.
But it is also an inaccurate process (as, again, some of the required
data is not in the replay, namely judgement counts of ticks
and so on).
For whatever's worth, a similar thing happens server-side in
https://github.com/ppy/osu-queue-score-statistics/blob/106c2948dbe695efcad5972d32cd46f4b36005cc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs#L319
- However, _ranks_ of stable scores will still use the local stable
reimplementation of ranks, i.e. a 1-miss stable score in osu! ruleset
will be an A rather than an S. See importer:
https://github.com/ppy/osu-queue-score-statistics/blob/106c2948dbe695efcad5972d32cd46f4b36005cc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs#L237
(it's the same method which is renamed
to `PopulateLegacyAccuracyAndRank()` in this commit).
That is all a bit of a mess honestly, but I'm not sure where to even
begin there...
2024-01-17 04:08:02 +08:00
|
|
|
|
byte[] compressedScoreInfo = null;
|
2020-03-31 16:13:42 +08:00
|
|
|
|
|
2018-02-28 15:34:47 +08:00
|
|
|
|
using (SerializationReader sr = new SerializationReader(stream))
|
|
|
|
|
{
|
2018-05-15 10:42:40 +08:00
|
|
|
|
currentRuleset = GetRuleset(sr.ReadByte());
|
2019-12-03 14:28:10 +08:00
|
|
|
|
var scoreInfo = new ScoreInfo { Ruleset = currentRuleset.RulesetInfo };
|
2019-03-27 15:55:46 +08:00
|
|
|
|
|
2019-03-27 15:59:29 +08:00
|
|
|
|
score.ScoreInfo = scoreInfo;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2021-10-27 12:04:41 +08:00
|
|
|
|
int version = sr.ReadInt32();
|
2023-06-08 20:24:40 +08:00
|
|
|
|
|
|
|
|
|
scoreInfo.IsLegacyScore = version < LegacyScoreEncoder.FIRST_LAZER_VERSION;
|
|
|
|
|
|
2023-07-15 11:19:18 +08:00
|
|
|
|
// TotalScoreVersion gets initialised to LATEST_VERSION.
|
|
|
|
|
// In the case where the incoming score has either an osu!stable or old lazer version, we need
|
|
|
|
|
// to mark it with the correct version increment to trigger reprocessing to new standardised scoring.
|
|
|
|
|
//
|
|
|
|
|
// See StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised().
|
|
|
|
|
scoreInfo.TotalScoreVersion = version < 30000002 ? 30000001 : LegacyScoreEncoder.LATEST_VERSION;
|
|
|
|
|
|
2023-01-10 03:51:38 +08:00
|
|
|
|
string beatmapHash = sr.ReadString();
|
|
|
|
|
|
|
|
|
|
workingBeatmap = GetBeatmap(beatmapHash);
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-11-28 18:48:15 +08:00
|
|
|
|
if (workingBeatmap is DummyWorkingBeatmap)
|
2023-01-10 03:51:38 +08:00
|
|
|
|
throw new BeatmapNotFoundException(beatmapHash);
|
2018-11-28 18:48:15 +08:00
|
|
|
|
|
2021-11-04 17:02:44 +08:00
|
|
|
|
scoreInfo.User = new APIUser { Username = sr.ReadString() };
|
2018-11-30 16:36:06 +08:00
|
|
|
|
|
|
|
|
|
// MD5Hash
|
|
|
|
|
sr.ReadString();
|
2018-05-11 19:31:57 +08:00
|
|
|
|
|
2019-12-03 14:28:10 +08:00
|
|
|
|
scoreInfo.SetCount300(sr.ReadUInt16());
|
|
|
|
|
scoreInfo.SetCount100(sr.ReadUInt16());
|
|
|
|
|
scoreInfo.SetCount50(sr.ReadUInt16());
|
|
|
|
|
scoreInfo.SetCountGeki(sr.ReadUInt16());
|
|
|
|
|
scoreInfo.SetCountKatu(sr.ReadUInt16());
|
|
|
|
|
scoreInfo.SetCountMiss(sr.ReadUInt16());
|
2018-05-11 19:31:57 +08:00
|
|
|
|
|
2019-03-27 15:55:46 +08:00
|
|
|
|
scoreInfo.TotalScore = sr.ReadInt32();
|
|
|
|
|
scoreInfo.MaxCombo = sr.ReadUInt16();
|
2018-11-28 16:02:14 +08:00
|
|
|
|
|
2018-02-28 15:34:47 +08:00
|
|
|
|
/* score.Perfect = */
|
|
|
|
|
sr.ReadBoolean();
|
2018-11-28 16:02:14 +08:00
|
|
|
|
|
2020-03-24 11:06:24 +08:00
|
|
|
|
scoreInfo.Mods = currentRuleset.ConvertFromLegacyMods((LegacyMods)sr.ReadInt32()).ToArray();
|
2018-11-28 16:02:14 +08:00
|
|
|
|
|
2021-06-08 17:00:09 +08:00
|
|
|
|
// lazer replays get a really high version number.
|
2021-06-08 17:38:47 +08:00
|
|
|
|
if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION)
|
2021-09-10 10:09:13 +08:00
|
|
|
|
scoreInfo.Mods = scoreInfo.Mods.Append(currentRuleset.CreateMod<ModClassic>()).ToArray();
|
2021-06-08 17:00:09 +08:00
|
|
|
|
|
2020-03-27 14:50:11 +08:00
|
|
|
|
currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods);
|
2021-10-04 16:35:53 +08:00
|
|
|
|
scoreInfo.BeatmapInfo = currentBeatmap.BeatmapInfo;
|
2020-03-27 14:50:11 +08:00
|
|
|
|
|
2022-03-24 14:49:13 +08:00
|
|
|
|
// As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing.
|
2022-03-24 15:43:41 +08:00
|
|
|
|
beatmapOffset = currentBeatmap.BeatmapInfo.BeatmapVersion < 5 ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0;
|
2022-03-24 14:49:13 +08:00
|
|
|
|
|
2018-02-28 15:34:47 +08:00
|
|
|
|
/* score.HpGraphString = */
|
|
|
|
|
sr.ReadString();
|
2018-11-28 16:02:14 +08:00
|
|
|
|
|
2019-03-27 15:55:46 +08:00
|
|
|
|
scoreInfo.Date = sr.ReadDateTime();
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2021-10-27 12:04:41 +08:00
|
|
|
|
byte[] compressedReplay = sr.ReadByteArray();
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-02-28 15:34:47 +08:00
|
|
|
|
if (version >= 20140721)
|
2023-09-01 14:10:26 +08:00
|
|
|
|
scoreInfo.LegacyOnlineID = sr.ReadInt64();
|
2018-02-28 15:34:47 +08:00
|
|
|
|
else if (version >= 20121008)
|
2023-09-01 14:10:26 +08:00
|
|
|
|
scoreInfo.LegacyOnlineID = sr.ReadInt32();
|
2019-07-29 17:36:07 +08:00
|
|
|
|
|
2022-12-06 19:10:51 +08:00
|
|
|
|
if (version >= 30000001)
|
|
|
|
|
compressedScoreInfo = sr.ReadByteArray();
|
2018-11-30 13:48:19 +08:00
|
|
|
|
|
2022-12-06 19:10:51 +08:00
|
|
|
|
if (compressedReplay?.Length > 0)
|
|
|
|
|
readCompressedData(compressedReplay, reader => readLegacyReplay(score.Replay, reader));
|
2018-11-30 13:48:19 +08:00
|
|
|
|
|
2022-12-06 19:10:51 +08:00
|
|
|
|
if (compressedScoreInfo?.Length > 0)
|
|
|
|
|
{
|
|
|
|
|
readCompressedData(compressedScoreInfo, reader =>
|
|
|
|
|
{
|
|
|
|
|
LegacyReplaySoloScoreInfo readScore = JsonConvert.DeserializeObject<LegacyReplaySoloScoreInfo>(reader.ReadToEnd());
|
2022-12-11 12:00:12 +08:00
|
|
|
|
|
|
|
|
|
Debug.Assert(readScore != null);
|
|
|
|
|
|
2023-09-01 14:23:29 +08:00
|
|
|
|
score.ScoreInfo.OnlineID = readScore.OnlineID;
|
2022-12-06 19:10:51 +08:00
|
|
|
|
score.ScoreInfo.Statistics = readScore.Statistics;
|
|
|
|
|
score.ScoreInfo.MaximumStatistics = readScore.MaximumStatistics;
|
|
|
|
|
score.ScoreInfo.Mods = readScore.Mods.Select(m => m.ToMod(currentRuleset)).ToArray();
|
2023-12-21 19:58:08 +08:00
|
|
|
|
score.ScoreInfo.ClientVersion = readScore.ClientVersion;
|
2022-12-06 19:10:51 +08:00
|
|
|
|
});
|
2018-05-11 19:32:06 +08:00
|
|
|
|
}
|
2018-11-30 13:48:19 +08:00
|
|
|
|
}
|
2018-05-11 19:32:06 +08:00
|
|
|
|
|
Fix incorrect accuracy and rank population when decoding lazer replays
Closes https://github.com/ppy/osu/issues/24061.
The gist of this change is that if the `LegacyReplaySoloScoreInfo`
bolt-on is present in the replay, then it can (and is) used to recompute
the accuracy, and rank is computed based on that.
This was the missing part of
https://github.com/ppy/osu/issues/24061#issuecomment-1888438151.
The accuracy would change on import before that because the encode
process is _lossy_ if the `LegacyReplaySoloScoreInfo` bolt-on is not
used, as the legacy format only has 6 fields for encoding judgement
counts, and some judgements that affect accuracy in lazer do not fit
into that.
Note that this _only_ fixes _relatively_ new lazer scores looking wrong
after reimport.
- Very old lazer scores, i.e. ones that don't have the
`LegacyReplaySoloScoreInfo` bolt-on, obviously can't use it
to repopulate. There's really not much good that can be done there,
so the stable pathways are used as a fallback that always works.
- For stable replays, `ScoreImporter` recalculates the accuracy of
the score _again_ in
https://github.com/ppy/osu/blob/15a5fd7e4c8e8e3c38382cfd3d5d676d107d7908/osu.Game/Scoring/ScoreImporter.cs#L106-L110
as `StandardisedScoreMigrationTools.UpdateFromLegacy()` recomputes
_both_ total score and accuracy.
This makes a _semblance_ of sense as it attempts to make the accuracy
of stable and lazer replays comparable. In most cases it also won't
matter, as the only ruleset where accuracy changed between the legacy
implementation and current lazer accuracy is mania.
But it is also an inaccurate process (as, again, some of the required
data is not in the replay, namely judgement counts of ticks
and so on).
For whatever's worth, a similar thing happens server-side in
https://github.com/ppy/osu-queue-score-statistics/blob/106c2948dbe695efcad5972d32cd46f4b36005cc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs#L319
- However, _ranks_ of stable scores will still use the local stable
reimplementation of ranks, i.e. a 1-miss stable score in osu! ruleset
will be an A rather than an S. See importer:
https://github.com/ppy/osu-queue-score-statistics/blob/106c2948dbe695efcad5972d32cd46f4b36005cc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs#L237
(it's the same method which is renamed
to `PopulateLegacyAccuracyAndRank()` in this commit).
That is all a bit of a mess honestly, but I'm not sure where to even
begin there...
2024-01-17 04:08:02 +08:00
|
|
|
|
if (score.ScoreInfo.IsLegacyScore || compressedScoreInfo == null)
|
|
|
|
|
PopulateLegacyAccuracyAndRank(score.ScoreInfo);
|
|
|
|
|
else
|
|
|
|
|
populateLazerAccuracyAndRank(score.ScoreInfo);
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2020-03-31 16:13:42 +08:00
|
|
|
|
// before returning for database import, we must restore the database-sourced BeatmapInfo.
|
|
|
|
|
// if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception.
|
2021-10-04 16:35:53 +08:00
|
|
|
|
score.ScoreInfo.BeatmapInfo = workingBeatmap.BeatmapInfo;
|
2023-02-07 16:52:47 +08:00
|
|
|
|
score.ScoreInfo.BeatmapHash = workingBeatmap.BeatmapInfo.Hash;
|
2020-03-31 16:13:42 +08:00
|
|
|
|
|
2018-11-30 13:48:19 +08:00
|
|
|
|
return score;
|
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2022-12-06 19:10:51 +08:00
|
|
|
|
private void readCompressedData(byte[] data, Action<StreamReader> readFunc)
|
|
|
|
|
{
|
|
|
|
|
using (var replayInStream = new MemoryStream(data))
|
|
|
|
|
{
|
|
|
|
|
byte[] properties = new byte[5];
|
|
|
|
|
if (replayInStream.Read(properties, 0, 5) != 5)
|
|
|
|
|
throw new IOException("input .lzma is too short");
|
|
|
|
|
|
|
|
|
|
long outSize = 0;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < 8; i++)
|
|
|
|
|
{
|
|
|
|
|
int v = replayInStream.ReadByte();
|
|
|
|
|
if (v < 0)
|
|
|
|
|
throw new IOException("Can't Read 1");
|
|
|
|
|
|
|
|
|
|
outSize |= (long)(byte)v << (8 * i);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
long compressedSize = replayInStream.Length - replayInStream.Position;
|
|
|
|
|
|
|
|
|
|
using (var lzma = new LzmaStream(properties, replayInStream, compressedSize, outSize))
|
|
|
|
|
using (var reader = new StreamReader(lzma))
|
|
|
|
|
readFunc(reader);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-17 19:45:48 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Populates the accuracy of a given <see cref="ScoreInfo"/> from its contained statistics.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// Legacy use only.
|
|
|
|
|
/// </remarks>
|
|
|
|
|
/// <param name="score">The <see cref="ScoreInfo"/> to populate.</param>
|
Fix incorrect accuracy and rank population when decoding lazer replays
Closes https://github.com/ppy/osu/issues/24061.
The gist of this change is that if the `LegacyReplaySoloScoreInfo`
bolt-on is present in the replay, then it can (and is) used to recompute
the accuracy, and rank is computed based on that.
This was the missing part of
https://github.com/ppy/osu/issues/24061#issuecomment-1888438151.
The accuracy would change on import before that because the encode
process is _lossy_ if the `LegacyReplaySoloScoreInfo` bolt-on is not
used, as the legacy format only has 6 fields for encoding judgement
counts, and some judgements that affect accuracy in lazer do not fit
into that.
Note that this _only_ fixes _relatively_ new lazer scores looking wrong
after reimport.
- Very old lazer scores, i.e. ones that don't have the
`LegacyReplaySoloScoreInfo` bolt-on, obviously can't use it
to repopulate. There's really not much good that can be done there,
so the stable pathways are used as a fallback that always works.
- For stable replays, `ScoreImporter` recalculates the accuracy of
the score _again_ in
https://github.com/ppy/osu/blob/15a5fd7e4c8e8e3c38382cfd3d5d676d107d7908/osu.Game/Scoring/ScoreImporter.cs#L106-L110
as `StandardisedScoreMigrationTools.UpdateFromLegacy()` recomputes
_both_ total score and accuracy.
This makes a _semblance_ of sense as it attempts to make the accuracy
of stable and lazer replays comparable. In most cases it also won't
matter, as the only ruleset where accuracy changed between the legacy
implementation and current lazer accuracy is mania.
But it is also an inaccurate process (as, again, some of the required
data is not in the replay, namely judgement counts of ticks
and so on).
For whatever's worth, a similar thing happens server-side in
https://github.com/ppy/osu-queue-score-statistics/blob/106c2948dbe695efcad5972d32cd46f4b36005cc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs#L319
- However, _ranks_ of stable scores will still use the local stable
reimplementation of ranks, i.e. a 1-miss stable score in osu! ruleset
will be an A rather than an S. See importer:
https://github.com/ppy/osu-queue-score-statistics/blob/106c2948dbe695efcad5972d32cd46f4b36005cc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs#L237
(it's the same method which is renamed
to `PopulateLegacyAccuracyAndRank()` in this commit).
That is all a bit of a mess honestly, but I'm not sure where to even
begin there...
2024-01-17 04:08:02 +08:00
|
|
|
|
public static void PopulateLegacyAccuracyAndRank(ScoreInfo score)
|
2018-11-30 13:48:19 +08:00
|
|
|
|
{
|
2020-08-28 00:05:06 +08:00
|
|
|
|
int countMiss = score.GetCountMiss() ?? 0;
|
|
|
|
|
int count50 = score.GetCount50() ?? 0;
|
|
|
|
|
int count100 = score.GetCount100() ?? 0;
|
|
|
|
|
int count300 = score.GetCount300() ?? 0;
|
|
|
|
|
int countGeki = score.GetCountGeki() ?? 0;
|
|
|
|
|
int countKatu = score.GetCountKatu() ?? 0;
|
2018-11-30 13:48:19 +08:00
|
|
|
|
|
2021-11-24 14:25:49 +08:00
|
|
|
|
switch (score.Ruleset.OnlineID)
|
2018-11-30 13:48:19 +08:00
|
|
|
|
{
|
|
|
|
|
case 0:
|
|
|
|
|
{
|
|
|
|
|
int totalHits = count50 + count100 + count300 + countMiss;
|
|
|
|
|
score.Accuracy = totalHits > 0 ? (double)(count50 * 50 + count100 * 100 + count300 * 300) / (totalHits * 300) : 1;
|
|
|
|
|
|
|
|
|
|
float ratio300 = (float)count300 / totalHits;
|
|
|
|
|
float ratio50 = (float)count50 / totalHits;
|
|
|
|
|
|
|
|
|
|
if (ratio300 == 1)
|
|
|
|
|
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X;
|
|
|
|
|
else if (ratio300 > 0.9 && ratio50 <= 0.01 && countMiss == 0)
|
|
|
|
|
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S;
|
2019-06-11 16:28:16 +08:00
|
|
|
|
else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9)
|
2018-11-30 13:48:19 +08:00
|
|
|
|
score.Rank = ScoreRank.A;
|
2019-06-11 16:28:16 +08:00
|
|
|
|
else if ((ratio300 > 0.7 && countMiss == 0) || ratio300 > 0.8)
|
2018-11-30 13:48:19 +08:00
|
|
|
|
score.Rank = ScoreRank.B;
|
|
|
|
|
else if (ratio300 > 0.6)
|
|
|
|
|
score.Rank = ScoreRank.C;
|
|
|
|
|
else
|
|
|
|
|
score.Rank = ScoreRank.D;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2019-04-01 10:39:02 +08:00
|
|
|
|
|
2018-11-30 13:48:19 +08:00
|
|
|
|
case 1:
|
|
|
|
|
{
|
|
|
|
|
int totalHits = count50 + count100 + count300 + countMiss;
|
|
|
|
|
score.Accuracy = totalHits > 0 ? (double)(count100 * 150 + count300 * 300) / (totalHits * 300) : 1;
|
|
|
|
|
|
|
|
|
|
float ratio300 = (float)count300 / totalHits;
|
|
|
|
|
float ratio50 = (float)count50 / totalHits;
|
|
|
|
|
|
|
|
|
|
if (ratio300 == 1)
|
|
|
|
|
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X;
|
|
|
|
|
else if (ratio300 > 0.9 && ratio50 <= 0.01 && countMiss == 0)
|
|
|
|
|
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S;
|
2019-06-11 16:28:16 +08:00
|
|
|
|
else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9)
|
2018-11-30 13:48:19 +08:00
|
|
|
|
score.Rank = ScoreRank.A;
|
2019-06-11 16:28:16 +08:00
|
|
|
|
else if ((ratio300 > 0.7 && countMiss == 0) || ratio300 > 0.8)
|
2018-11-30 13:48:19 +08:00
|
|
|
|
score.Rank = ScoreRank.B;
|
|
|
|
|
else if (ratio300 > 0.6)
|
|
|
|
|
score.Rank = ScoreRank.C;
|
|
|
|
|
else
|
|
|
|
|
score.Rank = ScoreRank.D;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2019-04-01 10:39:02 +08:00
|
|
|
|
|
2018-11-30 13:48:19 +08:00
|
|
|
|
case 2:
|
|
|
|
|
{
|
|
|
|
|
int totalHits = count50 + count100 + count300 + countMiss + countKatu;
|
|
|
|
|
score.Accuracy = totalHits > 0 ? (double)(count50 + count100 + count300) / totalHits : 1;
|
|
|
|
|
|
|
|
|
|
if (score.Accuracy == 1)
|
|
|
|
|
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X;
|
|
|
|
|
else if (score.Accuracy > 0.98)
|
|
|
|
|
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S;
|
|
|
|
|
else if (score.Accuracy > 0.94)
|
|
|
|
|
score.Rank = ScoreRank.A;
|
|
|
|
|
else if (score.Accuracy > 0.9)
|
|
|
|
|
score.Rank = ScoreRank.B;
|
|
|
|
|
else if (score.Accuracy > 0.85)
|
|
|
|
|
score.Rank = ScoreRank.C;
|
|
|
|
|
else
|
|
|
|
|
score.Rank = ScoreRank.D;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2019-04-01 10:39:02 +08:00
|
|
|
|
|
2018-11-30 13:48:19 +08:00
|
|
|
|
case 3:
|
|
|
|
|
{
|
|
|
|
|
int totalHits = count50 + count100 + count300 + countMiss + countGeki + countKatu;
|
|
|
|
|
score.Accuracy = totalHits > 0 ? (double)(count50 * 50 + count100 * 100 + countKatu * 200 + (count300 + countGeki) * 300) / (totalHits * 300) : 1;
|
|
|
|
|
|
|
|
|
|
if (score.Accuracy == 1)
|
|
|
|
|
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X;
|
|
|
|
|
else if (score.Accuracy > 0.95)
|
|
|
|
|
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S;
|
|
|
|
|
else if (score.Accuracy > 0.9)
|
|
|
|
|
score.Rank = ScoreRank.A;
|
|
|
|
|
else if (score.Accuracy > 0.8)
|
|
|
|
|
score.Rank = ScoreRank.B;
|
|
|
|
|
else if (score.Accuracy > 0.7)
|
|
|
|
|
score.Rank = ScoreRank.C;
|
|
|
|
|
else
|
|
|
|
|
score.Rank = ScoreRank.D;
|
|
|
|
|
break;
|
2018-02-28 15:34:47 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
Fix incorrect accuracy and rank population when decoding lazer replays
Closes https://github.com/ppy/osu/issues/24061.
The gist of this change is that if the `LegacyReplaySoloScoreInfo`
bolt-on is present in the replay, then it can (and is) used to recompute
the accuracy, and rank is computed based on that.
This was the missing part of
https://github.com/ppy/osu/issues/24061#issuecomment-1888438151.
The accuracy would change on import before that because the encode
process is _lossy_ if the `LegacyReplaySoloScoreInfo` bolt-on is not
used, as the legacy format only has 6 fields for encoding judgement
counts, and some judgements that affect accuracy in lazer do not fit
into that.
Note that this _only_ fixes _relatively_ new lazer scores looking wrong
after reimport.
- Very old lazer scores, i.e. ones that don't have the
`LegacyReplaySoloScoreInfo` bolt-on, obviously can't use it
to repopulate. There's really not much good that can be done there,
so the stable pathways are used as a fallback that always works.
- For stable replays, `ScoreImporter` recalculates the accuracy of
the score _again_ in
https://github.com/ppy/osu/blob/15a5fd7e4c8e8e3c38382cfd3d5d676d107d7908/osu.Game/Scoring/ScoreImporter.cs#L106-L110
as `StandardisedScoreMigrationTools.UpdateFromLegacy()` recomputes
_both_ total score and accuracy.
This makes a _semblance_ of sense as it attempts to make the accuracy
of stable and lazer replays comparable. In most cases it also won't
matter, as the only ruleset where accuracy changed between the legacy
implementation and current lazer accuracy is mania.
But it is also an inaccurate process (as, again, some of the required
data is not in the replay, namely judgement counts of ticks
and so on).
For whatever's worth, a similar thing happens server-side in
https://github.com/ppy/osu-queue-score-statistics/blob/106c2948dbe695efcad5972d32cd46f4b36005cc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs#L319
- However, _ranks_ of stable scores will still use the local stable
reimplementation of ranks, i.e. a 1-miss stable score in osu! ruleset
will be an A rather than an S. See importer:
https://github.com/ppy/osu-queue-score-statistics/blob/106c2948dbe695efcad5972d32cd46f4b36005cc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs#L237
(it's the same method which is renamed
to `PopulateLegacyAccuracyAndRank()` in this commit).
That is all a bit of a mess honestly, but I'm not sure where to even
begin there...
2024-01-17 04:08:02 +08:00
|
|
|
|
private void populateLazerAccuracyAndRank(ScoreInfo scoreInfo)
|
|
|
|
|
{
|
|
|
|
|
scoreInfo.Accuracy = StandardisedScoreMigrationTools.ComputeAccuracy(scoreInfo);
|
|
|
|
|
|
2024-01-19 17:43:15 +08:00
|
|
|
|
var rank = currentRuleset.CreateScoreProcessor().RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics);
|
Fix incorrect accuracy and rank population when decoding lazer replays
Closes https://github.com/ppy/osu/issues/24061.
The gist of this change is that if the `LegacyReplaySoloScoreInfo`
bolt-on is present in the replay, then it can (and is) used to recompute
the accuracy, and rank is computed based on that.
This was the missing part of
https://github.com/ppy/osu/issues/24061#issuecomment-1888438151.
The accuracy would change on import before that because the encode
process is _lossy_ if the `LegacyReplaySoloScoreInfo` bolt-on is not
used, as the legacy format only has 6 fields for encoding judgement
counts, and some judgements that affect accuracy in lazer do not fit
into that.
Note that this _only_ fixes _relatively_ new lazer scores looking wrong
after reimport.
- Very old lazer scores, i.e. ones that don't have the
`LegacyReplaySoloScoreInfo` bolt-on, obviously can't use it
to repopulate. There's really not much good that can be done there,
so the stable pathways are used as a fallback that always works.
- For stable replays, `ScoreImporter` recalculates the accuracy of
the score _again_ in
https://github.com/ppy/osu/blob/15a5fd7e4c8e8e3c38382cfd3d5d676d107d7908/osu.Game/Scoring/ScoreImporter.cs#L106-L110
as `StandardisedScoreMigrationTools.UpdateFromLegacy()` recomputes
_both_ total score and accuracy.
This makes a _semblance_ of sense as it attempts to make the accuracy
of stable and lazer replays comparable. In most cases it also won't
matter, as the only ruleset where accuracy changed between the legacy
implementation and current lazer accuracy is mania.
But it is also an inaccurate process (as, again, some of the required
data is not in the replay, namely judgement counts of ticks
and so on).
For whatever's worth, a similar thing happens server-side in
https://github.com/ppy/osu-queue-score-statistics/blob/106c2948dbe695efcad5972d32cd46f4b36005cc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs#L319
- However, _ranks_ of stable scores will still use the local stable
reimplementation of ranks, i.e. a 1-miss stable score in osu! ruleset
will be an A rather than an S. See importer:
https://github.com/ppy/osu-queue-score-statistics/blob/106c2948dbe695efcad5972d32cd46f4b36005cc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs#L237
(it's the same method which is renamed
to `PopulateLegacyAccuracyAndRank()` in this commit).
That is all a bit of a mess honestly, but I'm not sure where to even
begin there...
2024-01-17 04:08:02 +08:00
|
|
|
|
|
|
|
|
|
foreach (var mod in scoreInfo.Mods.OfType<IApplicableToScoreProcessor>())
|
|
|
|
|
rank = mod.AdjustRank(rank, scoreInfo.Accuracy);
|
|
|
|
|
|
|
|
|
|
scoreInfo.Rank = rank;
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-28 15:34:47 +08:00
|
|
|
|
private void readLegacyReplay(Replay replay, StreamReader reader)
|
|
|
|
|
{
|
2022-03-24 14:49:13 +08:00
|
|
|
|
float lastTime = beatmapOffset;
|
2019-09-13 22:05:47 +08:00
|
|
|
|
ReplayFrame currentFrame = null;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2021-10-27 12:04:41 +08:00
|
|
|
|
string[] frames = reader.ReadToEnd().Split(',');
|
2020-01-30 13:54:57 +08:00
|
|
|
|
|
2021-10-27 12:04:41 +08:00
|
|
|
|
for (int i = 0; i < frames.Length; i++)
|
2018-02-28 15:34:47 +08:00
|
|
|
|
{
|
2021-10-27 12:04:41 +08:00
|
|
|
|
string[] split = frames[i].Split('|');
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-02-28 15:34:47 +08:00
|
|
|
|
if (split.Length < 4)
|
|
|
|
|
continue;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-02-28 15:34:47 +08:00
|
|
|
|
if (split[0] == "-12345")
|
|
|
|
|
{
|
|
|
|
|
// Todo: The seed is provided in split[3], which we'll need to use at some point
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2021-10-27 12:04:41 +08:00
|
|
|
|
float diff = Parsing.ParseFloat(split[0]);
|
|
|
|
|
float mouseX = Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE);
|
|
|
|
|
float mouseY = Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE);
|
2020-01-30 13:54:57 +08:00
|
|
|
|
|
2018-02-28 15:34:47 +08:00
|
|
|
|
lastTime += diff;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2020-08-27 23:40:22 +08:00
|
|
|
|
if (i < 2 && mouseX == 256 && mouseY == -500)
|
|
|
|
|
// at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively.
|
|
|
|
|
// both frames use a position of (256, -500).
|
|
|
|
|
// ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania)
|
2020-01-30 13:54:57 +08:00
|
|
|
|
continue;
|
|
|
|
|
|
2018-02-28 15:34:47 +08:00
|
|
|
|
// Todo: At some point we probably want to rewind and play back the negative-time frames
|
|
|
|
|
// but for now we'll achieve equal playback to stable by skipping negative frames
|
|
|
|
|
if (diff < 0)
|
|
|
|
|
continue;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2019-09-13 22:05:47 +08:00
|
|
|
|
currentFrame = convertFrame(new LegacyReplayFrame(lastTime,
|
2020-08-27 23:40:22 +08:00
|
|
|
|
mouseX,
|
|
|
|
|
mouseY,
|
2019-09-13 22:05:47 +08:00
|
|
|
|
(ReplayButtonState)Parsing.ParseInt(split[3])), currentFrame);
|
2019-09-12 17:33:46 +08:00
|
|
|
|
|
2019-09-13 22:05:47 +08:00
|
|
|
|
replay.Frames.Add(currentFrame);
|
2018-02-28 15:34:47 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2019-09-13 22:05:47 +08:00
|
|
|
|
private ReplayFrame convertFrame(LegacyReplayFrame currentFrame, ReplayFrame lastFrame)
|
2018-02-28 15:34:47 +08:00
|
|
|
|
{
|
2018-03-01 00:32:32 +08:00
|
|
|
|
var convertible = currentRuleset.CreateConvertibleReplayFrame();
|
|
|
|
|
if (convertible == null)
|
|
|
|
|
throw new InvalidOperationException($"Legacy replay cannot be converted for the ruleset: {currentRuleset.Description}");
|
2019-02-28 12:31:40 +08:00
|
|
|
|
|
2020-03-25 19:21:34 +08:00
|
|
|
|
convertible.FromLegacy(currentFrame, currentBeatmap, lastFrame);
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-03-01 01:09:23 +08:00
|
|
|
|
var frame = (ReplayFrame)convertible;
|
2019-09-12 17:33:46 +08:00
|
|
|
|
frame.Time = currentFrame.Time;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-03-01 01:09:23 +08:00
|
|
|
|
return frame;
|
2018-02-28 15:34:47 +08:00
|
|
|
|
}
|
2018-05-15 10:42:40 +08:00
|
|
|
|
|
2018-05-15 14:27:57 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Retrieves the <see cref="Ruleset"/> for a specific id.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="rulesetId">The id.</param>
|
|
|
|
|
/// <returns>The <see cref="Ruleset"/>.</returns>
|
|
|
|
|
protected abstract Ruleset GetRuleset(int rulesetId);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Retrieves the <see cref="WorkingBeatmap"/> corresponding to an MD5 hash.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="md5Hash">The MD5 hash.</param>
|
|
|
|
|
/// <returns>The <see cref="WorkingBeatmap"/>.</returns>
|
|
|
|
|
protected abstract WorkingBeatmap GetBeatmap(string md5Hash);
|
2018-11-28 18:48:15 +08:00
|
|
|
|
|
|
|
|
|
public class BeatmapNotFoundException : Exception
|
|
|
|
|
{
|
2023-01-10 03:51:38 +08:00
|
|
|
|
public string Hash { get; }
|
|
|
|
|
|
|
|
|
|
public BeatmapNotFoundException(string hash)
|
2018-11-28 18:48:15 +08:00
|
|
|
|
{
|
2023-01-10 03:51:38 +08:00
|
|
|
|
Hash = hash;
|
2018-11-28 18:48:15 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-02-28 15:34:47 +08:00
|
|
|
|
}
|
|
|
|
|
}
|