1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-15 20:37:26 +08:00
osu-lazer/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

377 lines
16 KiB
C#
Raw Normal View History

// 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
using System;
using System.Diagnostics;
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;
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;
using osu.Game.IO.Legacy;
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;
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
{
2020-03-24 09:38:24 +08:00
public abstract class LegacyScoreDecoder
{
2018-04-19 19:44:38 +08:00
private IBeatmap currentBeatmap;
private Ruleset currentRuleset;
2018-04-13 17:19:50 +08:00
private float beatmapOffset;
public Score Parse(Stream stream)
{
var score = new Score
{
Replay = new Replay()
};
2018-04-13 17:19:50 +08:00
WorkingBeatmap workingBeatmap;
using (SerializationReader sr = new SerializationReader(stream))
{
currentRuleset = GetRuleset(sr.ReadByte());
var scoreInfo = new ScoreInfo { Ruleset = currentRuleset.RulesetInfo };
2019-03-27 15:59:29 +08:00
score.ScoreInfo = scoreInfo;
2018-04-13 17:19:50 +08:00
int version = sr.ReadInt32();
scoreInfo.IsLegacyScore = version < LegacyScoreEncoder.FIRST_LAZER_VERSION;
string beatmapHash = sr.ReadString();
workingBeatmap = GetBeatmap(beatmapHash);
2018-04-13 17:19:50 +08:00
if (workingBeatmap is DummyWorkingBeatmap)
throw new BeatmapNotFoundException(beatmapHash);
scoreInfo.User = new APIUser { Username = sr.ReadString() };
// MD5Hash
sr.ReadString();
2018-05-11 19:31:57 +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
scoreInfo.TotalScore = sr.ReadInt32();
scoreInfo.MaxCombo = sr.ReadUInt16();
/* score.Perfect = */
sr.ReadBoolean();
scoreInfo.Mods = currentRuleset.ConvertFromLegacyMods((LegacyMods)sr.ReadInt32()).ToArray();
// lazer replays get a really high version number.
2021-06-08 17:38:47 +08:00
if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION)
scoreInfo.Mods = scoreInfo.Mods.Append(currentRuleset.CreateMod<ModClassic>()).ToArray();
currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods);
scoreInfo.BeatmapInfo = currentBeatmap.BeatmapInfo;
// As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing.
beatmapOffset = currentBeatmap.BeatmapInfo.BeatmapVersion < 5 ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0;
/* score.HpGraphString = */
sr.ReadString();
scoreInfo.Date = sr.ReadDateTime();
2018-04-13 17:19:50 +08:00
byte[] compressedReplay = sr.ReadByteArray();
2018-04-13 17:19:50 +08:00
if (version >= 20140721)
2021-12-10 14:37:12 +08:00
scoreInfo.OnlineID = sr.ReadInt64();
else if (version >= 20121008)
2021-12-10 14:37:12 +08:00
scoreInfo.OnlineID = sr.ReadInt32();
2022-12-06 19:10:51 +08:00
byte[] compressedScoreInfo = null;
2019-02-28 12:31:40 +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());
Debug.Assert(readScore != null);
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();
});
2018-05-11 19:32:06 +08:00
}
2018-11-30 13:48:19 +08:00
}
2018-05-11 19:32:06 +08:00
2021-11-17 19:45:48 +08:00
PopulateAccuracy(score.ScoreInfo);
2018-04-13 17:19:50 +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.
score.ScoreInfo.BeatmapInfo = workingBeatmap.BeatmapInfo;
2023-02-07 16:52:47 +08:00
score.ScoreInfo.BeatmapHash = workingBeatmap.BeatmapInfo.Hash;
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>
public static void PopulateAccuracy(ScoreInfo score)
2018-11-30 13:48:19 +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
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;
else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9)
2018-11-30 13:48:19 +08:00
score.Rank = ScoreRank.A;
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;
}
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;
else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9)
2018-11-30 13:48:19 +08:00
score.Rank = ScoreRank.A;
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;
}
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;
}
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-04-13 17:19:50 +08:00
private void readLegacyReplay(Replay replay, StreamReader reader)
{
float lastTime = beatmapOffset;
bool skipFramesPresent = false;
ReplayFrame currentFrame = null;
2018-04-13 17:19:50 +08:00
// the negative time amount that must be "paid back" by positive frames before we start including frames again.
// When a negative frame occurs in a replay, all future frames are skipped until the sum total of their times
// is equal to or greater than the time of that negative frame.
// This value will be negative if we are in a time deficit, ie we have a negative frame that must be paid back.
// Otherwise it will be 0.
float timeDeficit = 0;
string[] frames = reader.ReadToEnd().Split(',');
for (int i = 0; i < frames.Length; i++)
{
string[] split = frames[i].Split('|');
2018-04-13 17:19:50 +08:00
if (split.Length < 4)
continue;
2018-04-13 17:19:50 +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
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);
lastTime += diff;
2018-04-13 17:19:50 +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)
skipFramesPresent = true;
continue;
}
// if the skip frames inserted by stable are present, the third frame will have a large negative time
// roughly equal to SkipBoundary. We don't want this to count towards the deficit: doing so would cause
// the replay data before the skip to be, well, skipped.
// In other words, this frame, if present, is a different kind of negative frame. It sets the "offset"
// for the beginning of the replay. This is the only negative frame to be handled in such a way.
bool isNegativeBreakFrame = i == 2 && skipFramesPresent && diff < 0;
if (!isNegativeBreakFrame)
{
timeDeficit += diff;
timeDeficit = Math.Min(0, timeDeficit);
}
// still paying back the deficit from a negative frame. Skip this frame.
// 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 (timeDeficit < 0 || isNegativeBreakFrame)
continue;
2018-04-13 17:19:50 +08:00
currentFrame = convertFrame(new LegacyReplayFrame(lastTime,
mouseX,
mouseY,
(ReplayButtonState)Parsing.ParseInt(split[3])), currentFrame);
replay.Frames.Add(currentFrame);
}
}
2018-04-13 17:19:50 +08:00
private ReplayFrame convertFrame(LegacyReplayFrame currentFrame, ReplayFrame lastFrame)
{
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
var frame = (ReplayFrame)convertible;
frame.Time = currentFrame.Time;
2018-04-13 17:19:50 +08:00
return frame;
}
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);
public class BeatmapNotFoundException : Exception
{
public string Hash { get; }
public BeatmapNotFoundException(string hash)
{
Hash = hash;
}
}
}
}