Append lazer score data to .osr files

This commit is contained in:
Dan Balasescu 2022-12-06 20:10:51 +09:00
parent e47f933cdc
commit df181acffe
3 changed files with 89 additions and 25 deletions

View File

@ -0,0 +1,38 @@
// 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 System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Scoring.Legacy
{
/// <summary>
/// A minified version of <see cref="SoloScoreInfo"/> retrofit onto the end of legacy replay files (.osr),
/// containing the minimum data required to support storage of non-legacy replays.
/// </summary>
[Serializable]
[JsonObject(MemberSerialization.OptIn)]
public class LegacyReplaySoloScoreInfo
{
[JsonProperty("mods")]
public APIMod[] Mods { get; set; } = Array.Empty<APIMod>();
[JsonProperty("statistics")]
public Dictionary<HitResult, int> Statistics { get; set; } = new Dictionary<HitResult, int>();
[JsonProperty("maximum_statistics")]
public Dictionary<HitResult, int> MaximumStatistics { get; set; } = new Dictionary<HitResult, int>();
public static LegacyReplaySoloScoreInfo FromScore(ScoreInfo score) => new LegacyReplaySoloScoreInfo
{
Mods = score.APIMods,
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
};
}
}

View File

@ -6,6 +6,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Newtonsoft.Json;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
@ -91,31 +92,23 @@ namespace osu.Game.Scoring.Legacy
else if (version >= 20121008) else if (version >= 20121008)
scoreInfo.OnlineID = sr.ReadInt32(); scoreInfo.OnlineID = sr.ReadInt32();
byte[] compressedScoreInfo = null;
if (version >= 30000001)
compressedScoreInfo = sr.ReadByteArray();
if (compressedReplay?.Length > 0) if (compressedReplay?.Length > 0)
readCompressedData(compressedReplay, reader => readLegacyReplay(score.Replay, reader));
if (compressedScoreInfo?.Length > 0)
{ {
using (var replayInStream = new MemoryStream(compressedReplay)) readCompressedData(compressedScoreInfo, reader =>
{ {
byte[] properties = new byte[5]; LegacyReplaySoloScoreInfo readScore = JsonConvert.DeserializeObject<LegacyReplaySoloScoreInfo>(reader.ReadToEnd());
if (replayInStream.Read(properties, 0, 5) != 5) score.ScoreInfo.Statistics = readScore.Statistics;
throw new IOException("input .lzma is too short"); score.ScoreInfo.MaximumStatistics = readScore.MaximumStatistics;
score.ScoreInfo.Mods = readScore.Mods.Select(m => m.ToMod(currentRuleset)).ToArray();
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))
readLegacyReplay(score.Replay, reader);
}
} }
} }
@ -128,6 +121,33 @@ namespace osu.Game.Scoring.Legacy
return score; return score;
} }
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);
}
}
/// <summary> /// <summary>
/// Populates the accuracy of a given <see cref="ScoreInfo"/> from its contained statistics. /// Populates the accuracy of a given <see cref="ScoreInfo"/> from its contained statistics.
/// </summary> /// </summary>

View File

@ -11,6 +11,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.IO.Legacy; using osu.Game.IO.Legacy;
using osu.Game.IO.Serialization;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
@ -29,7 +30,7 @@ namespace osu.Game.Scoring.Legacy
/// <summary> /// <summary>
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
/// </summary> /// </summary>
public const int FIRST_LAZER_VERSION = 30000000; public const int FIRST_LAZER_VERSION = 30000001;
private readonly Score score; private readonly Score score;
private readonly IBeatmap? beatmap; private readonly IBeatmap? beatmap;
@ -77,6 +78,7 @@ namespace osu.Game.Scoring.Legacy
sw.WriteByteArray(createReplayData()); sw.WriteByteArray(createReplayData());
sw.Write((long)0); sw.Write((long)0);
writeModSpecificData(score.ScoreInfo, sw); writeModSpecificData(score.ScoreInfo, sw);
sw.WriteByteArray(createScoreInfoData());
} }
} }
@ -84,9 +86,13 @@ namespace osu.Game.Scoring.Legacy
{ {
} }
private byte[] createReplayData() private byte[] createReplayData() => compress(replayStringContent);
private byte[] createScoreInfoData() => compress(LegacyReplaySoloScoreInfo.FromScore(score.ScoreInfo).Serialize());
private byte[] compress(string data)
{ {
byte[] content = new ASCIIEncoding().GetBytes(replayStringContent); byte[] content = new ASCIIEncoding().GetBytes(data);
using (var outStream = new MemoryStream()) using (var outStream = new MemoryStream())
{ {