2020-03-24 11:06:24 +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.
using System ;
2022-07-03 19:27:56 +08:00
using System.Diagnostics ;
2020-03-24 11:06:24 +08:00
using System.IO ;
2020-03-24 13:13:46 +08:00
using System.Linq ;
using System.Text ;
using osu.Framework.Extensions ;
using osu.Game.Beatmaps ;
2022-03-24 15:43:41 +08:00
using osu.Game.Beatmaps.Formats ;
2022-03-04 06:09:56 +08:00
using osu.Game.Extensions ;
2020-03-24 13:13:46 +08:00
using osu.Game.IO.Legacy ;
2022-12-06 19:10:51 +08:00
using osu.Game.IO.Serialization ;
2022-02-28 15:37:14 +08:00
using osu.Game.Replays.Legacy ;
2022-02-28 15:40:00 +08:00
using osu.Game.Rulesets.Replays ;
2020-03-24 13:13:46 +08:00
using osu.Game.Rulesets.Replays.Types ;
using SharpCompress.Compressors.LZMA ;
2020-03-24 11:06:24 +08:00
namespace osu.Game.Scoring.Legacy
{
public class LegacyScoreEncoder
{
2021-06-08 16:58:57 +08:00
/// <summary>
/// Database version in stable-compatible YYYYMMDD format.
2021-06-08 17:38:47 +08:00
/// Should be incremented if any changes are made to the format/usage.
2021-06-08 16:58:57 +08:00
/// </summary>
2022-12-14 17:30:31 +08:00
/// <remarks>
/// <list type="bullet">
/// <item><description>30000001: Appends <see cref="LegacyReplaySoloScoreInfo"/> to the end of scores.</description></item>
2023-07-04 16:53:53 +08:00
/// <item><description>30000002: Score stored to replay calculated using the Score V2 algorithm. Legacy scores on this version are candidate to Score V1 -> V2 conversion.</description></item>
2023-06-28 14:04:13 +08:00
/// <item><description>30000003: First version after converting legacy total score to standardised.</description></item>
2023-10-02 15:54:46 +08:00
/// <item><description>30000004: Fixed mod multipliers during legacy score conversion. Reconvert all scores.</description></item>
2023-11-20 06:53:05 +08:00
/// <item><description>30000005: Introduce combo exponent in the osu! gamemode. Reconvert all scores.</description></item>
2023-12-19 04:56:56 +08:00
/// <item><description>30000006: Fix edge cases in conversion after combo exponent introduction that lead to NaNs. Reconvert all scores.</description></item>
2023-12-20 22:58:26 +08:00
/// <item><description>30000007: Adjust osu!mania combo and accuracy portions and judgement scoring values. Reconvert all scores.</description></item>
2023-12-21 14:32:34 +08:00
/// <item><description>30000008: Add accuracy conversion. Reconvert all scores.</description></item>
2023-12-23 20:25:20 +08:00
/// <item><description>30000009: Fix edge cases in conversion for scores which have 0.0x mod multiplier on stable. Reconvert all scores.</description></item>
2022-12-14 17:30:31 +08:00
/// </list>
/// </remarks>
2023-12-23 20:25:20 +08:00
public const int LATEST_VERSION = 30000009 ;
2021-06-08 17:38:47 +08:00
/// <summary>
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
/// </summary>
2022-12-08 10:30:18 +08:00
public const int FIRST_LAZER_VERSION = 30000000 ;
2020-03-24 11:06:24 +08:00
private readonly Score score ;
2022-02-28 15:41:39 +08:00
private readonly IBeatmap ? beatmap ;
2020-03-24 11:06:24 +08:00
2022-02-28 15:37:14 +08:00
/// <summary>
/// Create a new score encoder for a specific score.
/// </summary>
/// <param name="score">The score to be encoded.</param>
/// <param name="beatmap">The beatmap used to convert frames for the score. May be null if the frames are already <see cref="LegacyReplayFrame"/>s.</param>
/// <exception cref="ArgumentException"></exception>
2022-02-28 15:41:39 +08:00
public LegacyScoreEncoder ( Score score , IBeatmap ? beatmap )
2020-03-24 11:06:24 +08:00
{
this . score = score ;
2020-03-24 13:13:46 +08:00
this . beatmap = beatmap ;
2020-03-24 11:06:24 +08:00
2022-02-28 15:37:14 +08:00
if ( beatmap = = null & & ! score . Replay . Frames . All ( f = > f is LegacyReplayFrame ) )
2022-02-28 15:41:39 +08:00
throw new ArgumentException ( @"Beatmap must be provided if frames are not already legacy frames." , nameof ( beatmap ) ) ;
2022-02-28 15:37:14 +08:00
2022-03-04 06:09:56 +08:00
if ( ! score . ScoreInfo . Ruleset . IsLegacyRuleset ( ) )
2022-02-28 15:41:39 +08:00
throw new ArgumentException ( @"Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format." , nameof ( score ) ) ;
2020-03-24 11:06:24 +08:00
}
2022-12-13 15:15:14 +08:00
public void Encode ( Stream stream , bool leaveOpen = false )
2020-03-24 11:06:24 +08:00
{
2022-12-13 15:15:14 +08:00
using ( SerializationWriter sw = new SerializationWriter ( stream , leaveOpen ) )
2020-03-24 13:13:46 +08:00
{
2021-11-24 14:25:49 +08:00
sw . Write ( ( byte ) ( score . ScoreInfo . Ruleset . OnlineID ) ) ;
2020-03-24 13:13:46 +08:00
sw . Write ( LATEST_VERSION ) ;
2023-07-04 13:50:34 +08:00
sw . Write ( score . ScoreInfo . BeatmapInfo ! . MD5Hash ) ;
2021-12-13 16:37:27 +08:00
sw . Write ( score . ScoreInfo . User . Username ) ;
sw . Write ( FormattableString . Invariant ( $"lazer-{score.ScoreInfo.User.Username}-{score.ScoreInfo.Date}" ) . ComputeMD5Hash ( ) ) ;
2020-03-24 13:13:46 +08:00
sw . Write ( ( ushort ) ( score . ScoreInfo . GetCount300 ( ) ? ? 0 ) ) ;
sw . Write ( ( ushort ) ( score . ScoreInfo . GetCount100 ( ) ? ? 0 ) ) ;
sw . Write ( ( ushort ) ( score . ScoreInfo . GetCount50 ( ) ? ? 0 ) ) ;
sw . Write ( ( ushort ) ( score . ScoreInfo . GetCountGeki ( ) ? ? 0 ) ) ;
sw . Write ( ( ushort ) ( score . ScoreInfo . GetCountKatu ( ) ? ? 0 ) ) ;
sw . Write ( ( ushort ) ( score . ScoreInfo . GetCountMiss ( ) ? ? 0 ) ) ;
sw . Write ( ( int ) ( score . ScoreInfo . TotalScore ) ) ;
sw . Write ( ( ushort ) score . ScoreInfo . MaxCombo ) ;
sw . Write ( score . ScoreInfo . Combo = = score . ScoreInfo . MaxCombo ) ;
sw . Write ( ( int ) score . ScoreInfo . Ruleset . CreateInstance ( ) . ConvertToLegacyMods ( score . ScoreInfo . Mods ) ) ;
2020-03-24 11:06:24 +08:00
2020-03-24 13:13:46 +08:00
sw . Write ( getHpGraphFormatted ( ) ) ;
sw . Write ( score . ScoreInfo . Date . DateTime ) ;
sw . WriteByteArray ( createReplayData ( ) ) ;
2023-09-01 14:10:26 +08:00
sw . Write ( score . ScoreInfo . LegacyOnlineID ) ;
2020-03-24 13:13:46 +08:00
writeModSpecificData ( score . ScoreInfo , sw ) ;
2022-12-06 19:10:51 +08:00
sw . WriteByteArray ( createScoreInfoData ( ) ) ;
2020-03-24 13:13:46 +08:00
}
2020-03-24 11:06:24 +08:00
}
2020-03-24 13:13:46 +08:00
private void writeModSpecificData ( ScoreInfo score , SerializationWriter sw )
2020-03-24 11:06:24 +08:00
{
}
2020-03-24 13:13:46 +08:00
2022-12-06 19:10:51 +08:00
private byte [ ] createReplayData ( ) = > compress ( replayStringContent ) ;
private byte [ ] createScoreInfoData ( ) = > compress ( LegacyReplaySoloScoreInfo . FromScore ( score . ScoreInfo ) . Serialize ( ) ) ;
private byte [ ] compress ( string data )
2020-03-24 13:13:46 +08:00
{
2022-12-06 19:10:51 +08:00
byte [ ] content = new ASCIIEncoding ( ) . GetBytes ( data ) ;
2020-03-24 13:13:46 +08:00
using ( var outStream = new MemoryStream ( ) )
{
using ( var lzma = new LzmaStream ( new LzmaEncoderProperties ( false , 1 < < 21 , 255 ) , false , outStream ) )
{
outStream . Write ( lzma . Properties ) ;
long fileSize = content . Length ;
for ( int i = 0 ; i < 8 ; i + + )
outStream . WriteByte ( ( byte ) ( fileSize > > ( 8 * i ) ) ) ;
lzma . Write ( content ) ;
}
return outStream . ToArray ( ) ;
}
}
private string replayStringContent
{
get
{
StringBuilder replayData = new StringBuilder ( ) ;
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
double offset = beatmap ? . BeatmapInfo . BeatmapVersion < 5 ? - LegacyBeatmapDecoder . EARLY_VERSION_TIMING_OFFSET : 0 ;
2022-03-24 14:49:13 +08:00
2023-06-23 23:59:36 +08:00
int lastTime = 0 ;
2021-04-28 20:55:20 +08:00
2023-06-24 21:35:07 +08:00
if ( score . Replay ! = null )
2023-06-23 23:59:36 +08:00
{
2023-06-24 21:35:07 +08:00
foreach ( var f in score . Replay . Frames )
{
var legacyFrame = getLegacyFrame ( f ) ;
// Rounding because stable could only parse integral values
int time = ( int ) Math . Round ( legacyFrame . Time + offset ) ;
replayData . Append ( FormattableString . Invariant ( $"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState}," ) ) ;
lastTime = time ;
}
2020-03-24 13:13:46 +08:00
}
2021-12-05 00:02:39 +08:00
// Warning: this is purposefully hardcoded as a string rather than interpolating, as in some cultures the minus sign is not encoded as the standard ASCII U+00C2 codepoint,
// which then would break decoding.
replayData . Append ( @"-12345|0|0|0" ) ;
2020-03-24 13:13:46 +08:00
return replayData . ToString ( ) ;
}
}
2022-02-28 15:40:00 +08:00
private LegacyReplayFrame getLegacyFrame ( ReplayFrame replayFrame )
{
2022-02-28 15:41:39 +08:00
switch ( replayFrame )
{
case LegacyReplayFrame legacyFrame :
return legacyFrame ;
2022-02-28 15:40:00 +08:00
2022-02-28 15:41:39 +08:00
case IConvertibleReplayFrame convertibleFrame :
2022-07-03 19:27:56 +08:00
Debug . Assert ( beatmap ! = null ) ;
return convertibleFrame . ToLegacy ( beatmap ) ;
2022-02-28 15:41:39 +08:00
default :
throw new ArgumentException ( @"Frame could not be converted to legacy frames" , nameof ( replayFrame ) ) ;
}
2022-02-28 15:40:00 +08:00
}
2020-03-24 13:13:46 +08:00
private string getHpGraphFormatted ( )
{
// todo: implement, maybe?
return string . Empty ;
}
2020-03-24 11:06:24 +08:00
}
}