2018-04-13 17:19:50 +08:00
|
|
|
|
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
|
|
|
|
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
|
|
|
|
|
|
|
|
|
using System;
|
|
|
|
|
using System.IO;
|
2018-11-28 15:12:57 +08:00
|
|
|
|
using System.Linq;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
using osu.Game.Beatmaps;
|
2018-11-28 15:12:57 +08:00
|
|
|
|
using osu.Game.Beatmaps.Legacy;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
using osu.Game.IO.Legacy;
|
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-04-13 17:19:50 +08:00
|
|
|
|
using osu.Game.Rulesets.Replays;
|
2018-11-28 15:12:57 +08:00
|
|
|
|
using osu.Game.Rulesets.Scoring;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
using osu.Game.Users;
|
|
|
|
|
using SharpCompress.Compressors.LZMA;
|
|
|
|
|
|
2018-11-28 15:12:57 +08:00
|
|
|
|
namespace osu.Game.Scoring.Legacy
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2018-05-15 14:27:57 +08:00
|
|
|
|
public abstract class LegacyScoreParser
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2018-04-19 19:44:38 +08:00
|
|
|
|
private IBeatmap currentBeatmap;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
private Ruleset currentRuleset;
|
|
|
|
|
|
2018-11-28 17:45:17 +08:00
|
|
|
|
public Score Parse(Stream stream)
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2018-11-28 17:45:17 +08:00
|
|
|
|
var score = new Score
|
|
|
|
|
{
|
|
|
|
|
ScoreInfo = new ScoreInfo(),
|
|
|
|
|
Replay = new Replay()
|
|
|
|
|
};
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
|
|
|
|
using (SerializationReader sr = new SerializationReader(stream))
|
|
|
|
|
{
|
2018-05-15 10:42:40 +08:00
|
|
|
|
currentRuleset = GetRuleset(sr.ReadByte());
|
2018-11-28 17:45:17 +08:00
|
|
|
|
score.ScoreInfo = new ScoreInfo { Ruleset = currentRuleset.RulesetInfo };
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
|
|
|
|
var version = sr.ReadInt32();
|
|
|
|
|
|
2018-11-28 18:48:15 +08:00
|
|
|
|
var workingBeatmap = GetBeatmap(sr.ReadString());
|
|
|
|
|
if (workingBeatmap is DummyWorkingBeatmap)
|
|
|
|
|
throw new BeatmapNotFoundException();
|
|
|
|
|
|
|
|
|
|
currentBeatmap = workingBeatmap.Beatmap;
|
2018-11-30 15:11:09 +08:00
|
|
|
|
score.ScoreInfo.Beatmap = currentBeatmap.BeatmapInfo;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-11-29 12:22:45 +08:00
|
|
|
|
score.ScoreInfo.User = new User { Username = sr.ReadString() };
|
2018-11-30 16:36:06 +08:00
|
|
|
|
|
|
|
|
|
// MD5Hash
|
|
|
|
|
sr.ReadString();
|
2018-05-11 19:31:57 +08:00
|
|
|
|
|
2018-11-30 13:48:19 +08:00
|
|
|
|
var count300 = (int)sr.ReadUInt16();
|
|
|
|
|
var count100 = (int)sr.ReadUInt16();
|
|
|
|
|
var count50 = (int)sr.ReadUInt16();
|
|
|
|
|
var countGeki = (int)sr.ReadUInt16();
|
|
|
|
|
var countKatu = (int)sr.ReadUInt16();
|
|
|
|
|
var countMiss = (int)sr.ReadUInt16();
|
2018-05-11 19:31:57 +08:00
|
|
|
|
|
2018-11-28 17:45:17 +08:00
|
|
|
|
score.ScoreInfo.Statistics[HitResult.Great] = count300;
|
|
|
|
|
score.ScoreInfo.Statistics[HitResult.Good] = count100;
|
|
|
|
|
score.ScoreInfo.Statistics[HitResult.Meh] = count50;
|
|
|
|
|
score.ScoreInfo.Statistics[HitResult.Perfect] = countGeki;
|
|
|
|
|
score.ScoreInfo.Statistics[HitResult.Ok] = countKatu;
|
|
|
|
|
score.ScoreInfo.Statistics[HitResult.Miss] = countMiss;
|
2018-05-11 19:31:57 +08:00
|
|
|
|
|
2018-11-28 17:45:17 +08:00
|
|
|
|
score.ScoreInfo.TotalScore = sr.ReadInt32();
|
|
|
|
|
score.ScoreInfo.MaxCombo = sr.ReadUInt16();
|
2018-11-28 16:02:14 +08:00
|
|
|
|
|
2018-04-13 17:19:50 +08:00
|
|
|
|
/* score.Perfect = */
|
|
|
|
|
sr.ReadBoolean();
|
2018-11-28 16:02:14 +08:00
|
|
|
|
|
2018-11-28 17:45:17 +08:00
|
|
|
|
score.ScoreInfo.Mods = currentRuleset.ConvertLegacyMods((LegacyMods)sr.ReadInt32()).ToArray();
|
2018-11-28 16:02:14 +08:00
|
|
|
|
|
2018-04-13 17:19:50 +08:00
|
|
|
|
/* score.HpGraphString = */
|
|
|
|
|
sr.ReadString();
|
2018-11-28 16:02:14 +08:00
|
|
|
|
|
2018-11-28 17:45:17 +08:00
|
|
|
|
score.ScoreInfo.Date = sr.ReadDateTime();
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
|
|
|
|
var compressedReplay = sr.ReadByteArray();
|
|
|
|
|
|
|
|
|
|
if (version >= 20140721)
|
2018-11-28 17:45:17 +08:00
|
|
|
|
score.ScoreInfo.OnlineScoreID = sr.ReadInt64();
|
2018-04-13 17:19:50 +08:00
|
|
|
|
else if (version >= 20121008)
|
2018-11-28 17:45:17 +08:00
|
|
|
|
score.ScoreInfo.OnlineScoreID = sr.ReadInt32();
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-11-30 13:48:19 +08:00
|
|
|
|
if (compressedReplay?.Length > 0)
|
2018-05-11 19:32:06 +08:00
|
|
|
|
{
|
2018-11-30 13:48:19 +08:00
|
|
|
|
using (var replayInStream = new MemoryStream(compressedReplay))
|
2018-05-11 19:32:06 +08:00
|
|
|
|
{
|
2018-11-30 13:48:19 +08:00
|
|
|
|
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))
|
|
|
|
|
readLegacyReplay(score.Replay, reader);
|
2018-05-11 19:32:06 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-11-30 13:48:19 +08:00
|
|
|
|
}
|
2018-05-11 19:32:06 +08:00
|
|
|
|
|
2018-11-30 13:48:19 +08:00
|
|
|
|
calculateAccuracy(score.ScoreInfo);
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-11-30 13:48:19 +08:00
|
|
|
|
return score;
|
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-11-30 13:48:19 +08:00
|
|
|
|
private void calculateAccuracy(ScoreInfo score)
|
|
|
|
|
{
|
|
|
|
|
int countMiss = (int)score.Statistics[HitResult.Miss];
|
|
|
|
|
int count50 = (int)score.Statistics[HitResult.Meh];
|
|
|
|
|
int count100 = (int)score.Statistics[HitResult.Good];
|
|
|
|
|
int count300 = (int)score.Statistics[HitResult.Great];
|
|
|
|
|
int countGeki = (int)score.Statistics[HitResult.Perfect];
|
|
|
|
|
int countKatu = (int)score.Statistics[HitResult.Ok];
|
|
|
|
|
|
|
|
|
|
switch (score.Ruleset.ID)
|
|
|
|
|
{
|
|
|
|
|
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)
|
|
|
|
|
score.Rank = ScoreRank.A;
|
|
|
|
|
else if (ratio300 > 0.7 && countMiss == 0 || ratio300 > 0.8)
|
|
|
|
|
score.Rank = ScoreRank.B;
|
|
|
|
|
else if (ratio300 > 0.6)
|
|
|
|
|
score.Rank = ScoreRank.C;
|
|
|
|
|
else
|
|
|
|
|
score.Rank = ScoreRank.D;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
score.Rank = ScoreRank.A;
|
|
|
|
|
else if (ratio300 > 0.7 && countMiss == 0 || ratio300 > 0.8)
|
|
|
|
|
score.Rank = ScoreRank.B;
|
|
|
|
|
else if (ratio300 > 0.6)
|
|
|
|
|
score.Rank = ScoreRank.C;
|
|
|
|
|
else
|
|
|
|
|
score.Rank = ScoreRank.D;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
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 = 0;
|
|
|
|
|
|
|
|
|
|
foreach (var l in reader.ReadToEnd().Split(','))
|
|
|
|
|
{
|
|
|
|
|
var split = l.Split('|');
|
|
|
|
|
|
|
|
|
|
if (split.Length < 4)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if (split[0] == "-12345")
|
|
|
|
|
{
|
|
|
|
|
// Todo: The seed is provided in split[3], which we'll need to use at some point
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var diff = float.Parse(split[0]);
|
|
|
|
|
lastTime += diff;
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
replay.Frames.Add(convertFrame(new LegacyReplayFrame(lastTime, float.Parse(split[1]), float.Parse(split[2]), (ReplayButtonState)int.Parse(split[3]))));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private ReplayFrame convertFrame(LegacyReplayFrame legacyFrame)
|
|
|
|
|
{
|
|
|
|
|
var convertible = currentRuleset.CreateConvertibleReplayFrame();
|
|
|
|
|
if (convertible == null)
|
|
|
|
|
throw new InvalidOperationException($"Legacy replay cannot be converted for the ruleset: {currentRuleset.Description}");
|
|
|
|
|
convertible.ConvertFrom(legacyFrame, currentBeatmap);
|
|
|
|
|
|
|
|
|
|
var frame = (ReplayFrame)convertible;
|
|
|
|
|
frame.Time = legacyFrame.Time;
|
|
|
|
|
|
|
|
|
|
return frame;
|
|
|
|
|
}
|
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
|
|
|
|
|
{
|
|
|
|
|
public BeatmapNotFoundException()
|
|
|
|
|
: base("No corresponding beatmap for the score could be found.")
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
}
|
|
|
|
|
}
|