2020-03-24 03:06:24 +00: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 05:13:46 +00:00
using System.Linq ;
using System.Text ;
using osu.Framework.Extensions ;
using osu.Game.Beatmaps ;
using osu.Game.IO.Legacy ;
using osu.Game.Rulesets.Replays.Types ;
using SharpCompress.Compressors.LZMA ;
2020-03-24 03:06:24 +00:00
namespace osu.Game.Scoring.Legacy
{
public class LegacyScoreEncoder
{
2021-06-08 08:58:57 +00:00
/// <summary>
/// Database version in stable-compatible YYYYMMDD format.
2021-06-08 09:38:47 +00:00
/// Should be incremented if any changes are made to the format/usage.
2021-06-08 08:58:57 +00:00
/// </summary>
2021-06-08 09:38:47 +00: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 03:06:24 +00:00
private readonly Score score ;
2020-03-24 05:13:46 +00:00
private readonly IBeatmap beatmap ;
2020-03-24 03:06:24 +00:00
2020-03-24 05:13:46 +00:00
public LegacyScoreEncoder ( Score score , IBeatmap beatmap )
2020-03-24 03:06:24 +00:00
{
this . score = score ;
2020-03-24 05:13:46 +00:00
this . beatmap = beatmap ;
2020-03-24 03:06:24 +00:00
2022-01-27 06:19:48 +00:00
if ( score . ScoreInfo . BeatmapInfo . Ruleset . OnlineID < 0 | | score . ScoreInfo . BeatmapInfo . Ruleset . OnlineID > 3 )
2020-03-24 03:06:24 +00: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 05:13:46 +00:00
public void Encode ( Stream stream )
2020-03-24 03:06:24 +00:00
{
2020-03-24 05:13:46 +00:00
using ( SerializationWriter sw = new SerializationWriter ( stream ) )
{
2021-11-24 06:25:49 +00:00
sw . Write ( ( byte ) ( score . ScoreInfo . Ruleset . OnlineID ) ) ;
2020-03-24 05:13:46 +00:00
sw . Write ( LATEST_VERSION ) ;
2021-10-04 08:35:53 +00:00
sw . Write ( score . ScoreInfo . BeatmapInfo . MD5Hash ) ;
2021-12-13 08:37:27 +00:00
sw . Write ( score . ScoreInfo . User . Username ) ;
sw . Write ( FormattableString . Invariant ( $"lazer-{score.ScoreInfo.User.Username}-{score.ScoreInfo.Date}" ) . ComputeMD5Hash ( ) ) ;
2020-03-24 05:13:46 +00: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 03:06:24 +00:00
2020-03-24 05:13:46 +00:00
sw . Write ( getHpGraphFormatted ( ) ) ;
sw . Write ( score . ScoreInfo . Date . DateTime ) ;
sw . WriteByteArray ( createReplayData ( ) ) ;
sw . Write ( ( long ) 0 ) ;
writeModSpecificData ( score . ScoreInfo , sw ) ;
}
2020-03-24 03:06:24 +00:00
}
2020-03-24 05:13:46 +00:00
private void writeModSpecificData ( ScoreInfo score , SerializationWriter sw )
2020-03-24 03:06:24 +00:00
{
}
2020-03-24 05:13:46 +00:00
private byte [ ] createReplayData ( )
{
2021-10-27 04:04:41 +00:00
byte [ ] content = new ASCIIEncoding ( ) . GetBytes ( replayStringContent ) ;
2020-03-24 05:13:46 +00: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 ( ) ;
if ( score . Replay ! = null )
{
2021-04-28 12:55:20 +00:00
int lastTime = 0 ;
2020-03-25 11:21:34 +00:00
foreach ( var f in score . Replay . Frames . OfType < IConvertibleReplayFrame > ( ) . Select ( f = > f . ToLegacy ( beatmap ) ) )
2020-03-24 05:13:46 +00:00
{
2021-04-28 12:23:56 +00:00
// Rounding because stable could only parse integral values
2021-04-28 12:55:20 +00:00
int time = ( int ) Math . Round ( f . Time ) ;
replayData . Append ( FormattableString . Invariant ( $"{time - lastTime}|{f.MouseX ?? 0}|{f.MouseY ?? 0}|{(int)f.ButtonState}," ) ) ;
lastTime = time ;
2020-03-24 05:13:46 +00:00
}
}
2021-12-04 16:02:39 +00: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 05:13:46 +00:00
return replayData . ToString ( ) ;
}
}
private string getHpGraphFormatted ( )
{
// todo: implement, maybe?
return string . Empty ;
}
2020-03-24 03:06:24 +00:00
}
}