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 ;
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-04 06:09:56 +08:00
using osu.Game.Extensions ;
2020-03-24 13:13:46 +08:00
using osu.Game.IO.Legacy ;
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
2022-02-28 15:41:39 +08:00
#nullable enable
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>
2021-06-08 17:38:47 +08:00
public const int LATEST_VERSION = FIRST_LAZER_VERSION ;
/// <summary>
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
/// </summary>
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
}
2020-03-24 13:13:46 +08:00
public void Encode ( Stream stream )
2020-03-24 11:06:24 +08:00
{
2020-03-24 13:13:46 +08:00
using ( SerializationWriter sw = new SerializationWriter ( stream ) )
{
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 ) ;
2021-10-04 16:35:53 +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 ( ) ) ;
sw . Write ( ( long ) 0 ) ;
writeModSpecificData ( score . ScoreInfo , sw ) ;
}
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
private byte [ ] createReplayData ( )
{
2021-10-27 12:04:41 +08:00
byte [ ] content = new ASCIIEncoding ( ) . GetBytes ( replayStringContent ) ;
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
// BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off)
// As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing.
double offset = beatmap ? . BeatmapInfo . BeatmapVersion < 5 ? - 24 : 0 ;
2020-03-24 13:13:46 +08:00
if ( score . Replay ! = null )
{
2021-04-28 20:55:20 +08:00
int lastTime = 0 ;
2022-02-28 15:40:00 +08:00
foreach ( var f in score . Replay . Frames )
2020-03-24 13:13:46 +08:00
{
2022-02-28 15:40:00 +08:00
var legacyFrame = getLegacyFrame ( f ) ;
2021-04-28 20:23:56 +08:00
// Rounding because stable could only parse integral values
2022-03-24 14:49:13 +08:00
int time = ( int ) Math . Round ( legacyFrame . Time + offset ) ;
2022-02-28 15:40:00 +08:00
replayData . Append ( FormattableString . Invariant ( $"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState}," ) ) ;
2021-04-28 20:55:20 +08:00
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 :
return convertibleFrame . ToLegacy ( beatmap ) ;
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
}
}