@@ -10,6 +10,10 @@ namespace GeometryDashAPI.Data
10
10
{
11
11
public class GameData
12
12
{
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
+
13
17
public Plist DataPlist { get ; set ; }
14
18
15
19
private readonly GameDataType ? type ;
@@ -34,27 +38,41 @@ public virtual async Task LoadAsync(string fileName)
34
38
using var file = new FileStream ( fileName , FileMode . Open , FileAccess . Read , FileShare . Read , 4096 , useAsync : true ) ;
35
39
#endif
36
40
var data = new byte [ file . Length ] ;
37
- await file . ReadAsync ( data , 0 , data . Length ) ;
41
+ _ = await file . ReadAsync ( data , 0 , data . Length ) ;
42
+
43
+ if ( data . AsSpan ( ) . Slice ( 0 , XorDatFileMagickBytes . Length ) . IndexOf ( XorDatFileMagickBytes ) != 0 )
44
+ {
45
+ // mac files
46
+ var decryptedData = Crypt . LoadSaveAsMacOS ( data ) ;
47
+ DataPlist = new Plist ( Encoding . ASCII . GetBytes ( decryptedData ) ) ;
48
+ return ;
49
+ }
38
50
51
+ // windows files
39
52
var xor = Crypt . XOR ( data , 0xB ) ;
40
53
var index = xor . AsSpan ( ) . IndexOf ( ( byte ) 0 ) ;
41
- var gZipDecompress = Crypt . GZipDecompress ( GameConvert . FromBase64 ( Encoding . ASCII . GetString ( xor , 0 , index >= 0 ? index : xor . Length ) ) ) ;
42
-
54
+ var gZipDecompress =
55
+ Crypt . GZipDecompress (
56
+ GameConvert . FromBase64 ( Encoding . ASCII . GetString ( xor , 0 , index >= 0 ? index : xor . Length ) ) ) ;
43
57
DataPlist = new Plist ( Encoding . ASCII . GetBytes ( gZipDecompress ) ) ;
44
58
}
45
-
59
+
46
60
/// <summary>
47
61
/// Saves class data to a file as a game save<br/><br/>
48
62
/// Before saving, make sure that you have closed the game. Otherwise, after closing, the game will overwrite the file<br/>
49
63
/// </summary>
50
64
/// <param name="fullName">File to write the data.<br />
51
65
/// use <b>null</b> value for default resolving
52
66
/// </param>
53
- 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 )
54
72
{
55
73
using var memory = new MemoryStream ( ) ;
56
74
DataPlist . SaveToStream ( memory ) ;
57
- File . WriteAllBytes ( fullName ?? ResolveFileName ( type ) , GetFileContent ( memory ) ) ;
75
+ File . WriteAllBytes ( fullName ?? ResolveFileName ( type ) , GetFileContent ( memory , format ?? ResolveFileFormat ( ) ) ) ;
58
76
}
59
77
60
78
/// <summary>
@@ -64,15 +82,19 @@ public void Save(string? fullName = null)
64
82
/// <param name="fileName">File to write the data.<br />
65
83
/// use <b>null</b> value for default resolving
66
84
/// </param>
67
- 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 )
68
90
{
69
91
using var memory = new MemoryStream ( ) ;
70
92
await DataPlist . SaveToStreamAsync ( memory ) ;
71
93
#if NETSTANDARD2_1
72
- await File . WriteAllBytesAsync ( fileName ?? ResolveFileName ( type ) , GetFileContent ( memory ) ) ;
94
+ await File . WriteAllBytesAsync ( fileName ?? ResolveFileName ( type ) , GetFileContent ( memory , format ?? ResolveFileFormat ( ) ) ) ;
73
95
#else
74
96
using var file = new FileStream ( fileName ?? ResolveFileName ( type ) , FileMode . Create , FileAccess . ReadWrite , FileShare . Read , 4096 , useAsync : true ) ;
75
- var data = GetFileContent ( memory ) ;
97
+ var data = GetFileContent ( memory , format ?? ResolveFileFormat ( ) ) ;
76
98
await file . WriteAsync ( data , 0 , data . Length ) ;
77
99
#endif
78
100
}
@@ -83,13 +105,40 @@ public static string ResolveFileName(GameDataType? type)
83
105
throw new InvalidOperationException ( "can't resolve the directory with the saves for undefined file type. Use certain file name" ) ;
84
106
if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
85
107
return $@ "{ Environment . GetEnvironmentVariable ( "LocalAppData" ) } \GeometryDash\CC{ type } .dat";
108
+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . OSX ) )
109
+ return $ "/Users/{ Environment . GetEnvironmentVariable ( "USER" ) } /Library/Application Support/GeometryDash/CC{ type } .dat";
86
110
throw new InvalidOperationException ( $ "can't resolve the directory with the saves on your operating system: '{ RuntimeInformation . OSDescription } '. Use certain file name") ;
87
111
}
88
112
89
- private static byte [ ] GetFileContent ( MemoryStream memory )
113
+ public static DatFileFormat ResolveFileFormat ( )
90
114
{
115
+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . OSX ) )
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 ( ) ) ;
124
+
91
125
var base64 = GameConvert . ToBase64 ( Crypt . GZipCompress ( memory . ToArray ( ) ) ) ;
92
126
return Crypt . XOR ( Encoding . ASCII . GetBytes ( base64 ) , 0xB ) ;
93
127
}
128
+
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 ;
142
+ }
94
143
}
95
144
}
0 commit comments