@@ -10,6 +10,10 @@ namespace GeometryDashAPI.Data
1010{
1111 public class GameData
1212 {
13+ // This is xored gzip magick bytes: 'C?'
14+ // see more https://en.wikipedia.org/wiki/Gzip
15+ private static readonly byte [ ] XorDatFileMagickBytes = [ 0x43 , 0x3f ] ;
16+
1317 public Plist DataPlist { get ; set ; }
1418
1519 private readonly GameDataType ? type ;
@@ -34,36 +38,41 @@ public virtual async Task LoadAsync(string fileName)
3438 using var file = new FileStream ( fileName , FileMode . Open , FileAccess . Read , FileShare . Read , 4096 , useAsync : true ) ;
3539#endif
3640 var data = new byte [ file . Length ] ;
37- await file . ReadAsync ( data , 0 , data . Length ) ;
41+ _ = await file . ReadAsync ( data , 0 , data . Length ) ;
3842
39- if ( RuntimeInformation . IsOSPlatform ( OSPlatform . OSX ) )
43+ if ( data . AsSpan ( ) . Slice ( 0 , XorDatFileMagickBytes . Length ) . IndexOf ( XorDatFileMagickBytes ) != 0 )
4044 {
41- string decryptedData = Crypt . LoadSaveAsMacOS ( data ) ;
45+ // mac files
46+ var decryptedData = Crypt . LoadSaveAsMacOS ( data ) ;
4247 DataPlist = new Plist ( Encoding . ASCII . GetBytes ( decryptedData ) ) ;
48+ return ;
4349 }
44- else
45- {
46- var xor = Crypt . XOR ( data , 0xB ) ;
47- var index = xor . AsSpan ( ) . IndexOf ( ( byte ) 0 ) ;
48- var gZipDecompress =
49- Crypt . GZipDecompress (
50- GameConvert . FromBase64 ( Encoding . ASCII . GetString ( xor , 0 , index >= 0 ? index : xor . Length ) ) ) ;
51- DataPlist = new Plist ( Encoding . ASCII . GetBytes ( gZipDecompress ) ) ;
52- }
50+
51+ // windows files
52+ var xor = Crypt . XOR ( data , 0xB ) ;
53+ var index = xor . AsSpan ( ) . IndexOf ( ( byte ) 0 ) ;
54+ var gZipDecompress =
55+ Crypt . GZipDecompress (
56+ GameConvert . FromBase64 ( Encoding . ASCII . GetString ( xor , 0 , index >= 0 ? index : xor . Length ) ) ) ;
57+ DataPlist = new Plist ( Encoding . ASCII . GetBytes ( gZipDecompress ) ) ;
5358 }
54-
59+
5560 /// <summary>
5661 /// Saves class data to a file as a game save<br/><br/>
5762 /// Before saving, make sure that you have closed the game. Otherwise, after closing, the game will overwrite the file<br/>
5863 /// </summary>
5964 /// <param name="fullName">File to write the data.<br />
6065 /// use <b>null</b> value for default resolving
6166 /// </param>
62- public void Save ( string ? fullName = null )
67+ /// <param name="format">
68+ /// Specify if you want to save the file in a format specific to another operating system.<br />
69+ /// Leave <b>null</b> to save the file for the current operating system
70+ /// </param>
71+ public void Save ( string ? fullName = null , DatFileFormat ? format = null )
6372 {
6473 using var memory = new MemoryStream ( ) ;
6574 DataPlist . SaveToStream ( memory ) ;
66- File . WriteAllBytes ( fullName ?? ResolveFileName ( type ) , GetFileContent ( memory ) ) ;
75+ File . WriteAllBytes ( fullName ?? ResolveFileName ( type ) , GetFileContent ( memory , format ?? ResolveFileFormat ( ) ) ) ;
6776 }
6877
6978 /// <summary>
@@ -73,15 +82,19 @@ public void Save(string? fullName = null)
7382 /// <param name="fileName">File to write the data.<br />
7483 /// use <b>null</b> value for default resolving
7584 /// </param>
76- public async Task SaveAsync ( string ? fileName = null )
85+ /// <param name="format">
86+ /// Specify if you want to save the file in a format specific to another operating system.<br />
87+ /// Leave <b>null</b> to save the file for the current operating system
88+ /// </param>
89+ public async Task SaveAsync ( string ? fileName = null , DatFileFormat ? format = null )
7790 {
7891 using var memory = new MemoryStream ( ) ;
7992 await DataPlist . SaveToStreamAsync ( memory ) ;
8093#if NETSTANDARD2_1
81- await File . WriteAllBytesAsync ( fileName ?? ResolveFileName ( type ) , GetFileContent ( memory ) ) ;
94+ await File . WriteAllBytesAsync ( fileName ?? ResolveFileName ( type ) , GetFileContent ( memory , format ?? ResolveFileFormat ( ) ) ) ;
8295#else
8396 using var file = new FileStream ( fileName ?? ResolveFileName ( type ) , FileMode . Create , FileAccess . ReadWrite , FileShare . Read , 4096 , useAsync : true ) ;
84- var data = GetFileContent ( memory ) ;
97+ var data = GetFileContent ( memory , format ?? ResolveFileFormat ( ) ) ;
8598 await file . WriteAsync ( data , 0 , data . Length ) ;
8699#endif
87100 }
@@ -93,21 +106,39 @@ public static string ResolveFileName(GameDataType? type)
93106 if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
94107 return $@ "{ Environment . GetEnvironmentVariable ( "LocalAppData" ) } \GeometryDash\CC{ type } .dat";
95108 if ( RuntimeInformation . IsOSPlatform ( OSPlatform . OSX ) )
96- return $@ "/Users/{ Environment . GetEnvironmentVariable ( "USER" ) } /Library/Application Support/GeometryDash/CC{ type } .dat";
109+ return $ "/Users/{ Environment . GetEnvironmentVariable ( "USER" ) } /Library/Application Support/GeometryDash/CC{ type } .dat";
97110 throw new InvalidOperationException ( $ "can't resolve the directory with the saves on your operating system: '{ RuntimeInformation . OSDescription } '. Use certain file name") ;
98111 }
99112
100- private static byte [ ] GetFileContent ( MemoryStream memory )
113+ public static DatFileFormat ResolveFileFormat ( )
101114 {
102115 if ( RuntimeInformation . IsOSPlatform ( OSPlatform . OSX ) )
103- {
104- var encrypted = Crypt . SavingSaveAsMacOS ( memory . ToArray ( ) ) ;
105- return encrypted ;
106- }
116+ return DatFileFormat . Mac ;
117+ return DatFileFormat . Windows ;
118+ }
119+
120+ private static byte [ ] GetFileContent ( MemoryStream memory , DatFileFormat format )
121+ {
122+ if ( format == DatFileFormat . Mac )
123+ return Crypt . SavingSaveAsMacOS ( memory . ToArray ( ) ) ;
107124
108125 var base64 = GameConvert . ToBase64 ( Crypt . GZipCompress ( memory . ToArray ( ) ) ) ;
109126 return Crypt . XOR ( Encoding . ASCII . GetBytes ( base64 ) , 0xB ) ;
127+ }
110128
129+ private static bool StartsWith ( Stream stream , ReadOnlySpan < byte > prefix )
130+ {
131+ if ( ! stream . CanSeek )
132+ throw new ArgumentException ( $ "{ nameof ( stream ) } is not seekable. This can lead to bugs.") ;
133+ if ( stream . Length < prefix . Length )
134+ return false ;
135+ var position = stream . Position ;
136+ var buffer = new byte [ prefix . Length ] ;
137+ var read = 0 ;
138+ while ( read != buffer . Length )
139+ read += stream . Read ( buffer , read , buffer . Length - read ) ;
140+ stream . Seek ( position , SeekOrigin . Begin ) ;
141+ return buffer . AsSpan ( ) . IndexOf ( prefix ) == 0 ;
111142 }
112143 }
113144}
0 commit comments