Skip to content

Commit 00efdad

Browse files
authored
Merge pull request #33 from delynith/feature/macos_save_support
adding macos support to save files
2 parents 3740937 + bfd7749 commit 00efdad

File tree

4 files changed

+124
-11
lines changed

4 files changed

+124
-11
lines changed

GeometryDashAPI.sln.DotSettings

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2-
<s:Boolean x:Key="/Default/UserDictionary/Words/=LEVELCONTENT/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
2+
<s:Boolean x:Key="/Default/UserDictionary/Words/=LEVELCONTENT/@EntryIndexedValue">True</s:Boolean>
3+
<s:Boolean x:Key="/Default/UserDictionary/Words/=xored/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

GeometryDashAPI/Crypt.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
using System.IO;
22
using System.IO.Compression;
3+
using System.Security.Cryptography;
34
using System.Text;
45
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
56

67
namespace GeometryDashAPI
78
{
89
public class Crypt
910
{
11+
// The MacOS save file is not encoded like the Windows one - instead, it uses AES ECB encryption.
12+
// Huge thanks to: https://github.com/qimiko/gd-save-tools/blob/b5176eb2c805ca65da3e51701409b72b90bdd497/assets/js/savefile.mjs#L43
13+
private static byte[] MAC_SAVE_KEY =
14+
[
15+
0x69, 0x70, 0x75, 0x39, 0x54, 0x55, 0x76, 0x35,
16+
0x34, 0x79, 0x76, 0x5D, 0x69, 0x73, 0x46, 0x4D,
17+
0x68, 0x35, 0x40, 0x3B, 0x74, 0x2E, 0x35, 0x77,
18+
0x33, 0x34, 0x45, 0x32, 0x52, 0x79, 0x40, 0x7B
19+
];
20+
1021
public static byte[] XOR(byte[] data, int key)
1122
{
1223
var result = new byte[data.Length];
@@ -53,5 +64,50 @@ public static byte[] GZipCompress(byte[] data)
5364
}
5465
return memory.ToArray();
5566
}
67+
68+
public static byte[] SavingSaveAsMacOS(byte[] data)
69+
{
70+
using (Aes aesAlg = Aes.Create())
71+
{
72+
aesAlg.Key = MAC_SAVE_KEY;
73+
aesAlg.Mode = CipherMode.ECB;
74+
75+
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
76+
77+
using (MemoryStream msEncrypt = new MemoryStream())
78+
{
79+
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
80+
{
81+
csEncrypt.Write(data, 0, data.Length);
82+
}
83+
return msEncrypt.ToArray();
84+
}
85+
}
86+
}
87+
88+
public static string LoadSaveAsMacOS(byte[] data)
89+
{
90+
using (Aes aesAlg = Aes.Create())
91+
{
92+
aesAlg.Key = MAC_SAVE_KEY;
93+
aesAlg.Mode = CipherMode.ECB;
94+
95+
// Create a decryptor to perform the stream transform.
96+
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
97+
98+
// Create the streams used for decryption.
99+
using (MemoryStream msDecrypt = new MemoryStream(data))
100+
{
101+
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
102+
{
103+
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
104+
{
105+
// Read the decrypted bytes from the decrypting stream and place them in a string.
106+
return srDecrypt.ReadToEnd();
107+
}
108+
}
109+
}
110+
}
111+
}
56112
}
57113
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace GeometryDashAPI.Data;
2+
3+
public enum DatFileFormat
4+
{
5+
Windows,
6+
Mac
7+
}

GeometryDashAPI/Data/GameData.cs

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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,27 +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);
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+
}
3850

51+
// windows files
3952
var xor = Crypt.XOR(data, 0xB);
4053
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)));
4357
DataPlist = new Plist(Encoding.ASCII.GetBytes(gZipDecompress));
4458
}
45-
59+
4660
/// <summary>
4761
/// Saves class data to a file as a game save<br/><br/>
4862
/// Before saving, make sure that you have closed the game. Otherwise, after closing, the game will overwrite the file<br/>
4963
/// </summary>
5064
/// <param name="fullName">File to write the data.<br />
5165
/// use <b>null</b> value for default resolving
5266
/// </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)
5472
{
5573
using var memory = new MemoryStream();
5674
DataPlist.SaveToStream(memory);
57-
File.WriteAllBytes(fullName ?? ResolveFileName(type), GetFileContent(memory));
75+
File.WriteAllBytes(fullName ?? ResolveFileName(type), GetFileContent(memory, format ?? ResolveFileFormat()));
5876
}
5977

6078
/// <summary>
@@ -64,15 +82,19 @@ public void Save(string? fullName = null)
6482
/// <param name="fileName">File to write the data.<br />
6583
/// use <b>null</b> value for default resolving
6684
/// </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)
6890
{
6991
using var memory = new MemoryStream();
7092
await DataPlist.SaveToStreamAsync(memory);
7193
#if NETSTANDARD2_1
72-
await File.WriteAllBytesAsync(fileName ?? ResolveFileName(type), GetFileContent(memory));
94+
await File.WriteAllBytesAsync(fileName ?? ResolveFileName(type), GetFileContent(memory, format ?? ResolveFileFormat()));
7395
#else
7496
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());
7698
await file.WriteAsync(data, 0, data.Length);
7799
#endif
78100
}
@@ -83,13 +105,40 @@ public static string ResolveFileName(GameDataType? type)
83105
throw new InvalidOperationException("can't resolve the directory with the saves for undefined file type. Use certain file name");
84106
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
85107
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";
86110
throw new InvalidOperationException($"can't resolve the directory with the saves on your operating system: '{RuntimeInformation.OSDescription}'. Use certain file name");
87111
}
88112

89-
private static byte[] GetFileContent(MemoryStream memory)
113+
public static DatFileFormat ResolveFileFormat()
90114
{
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+
91125
var base64 = GameConvert.ToBase64(Crypt.GZipCompress(memory.ToArray()));
92126
return Crypt.XOR(Encoding.ASCII.GetBytes(base64), 0xB);
93127
}
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+
}
94143
}
95144
}

0 commit comments

Comments
 (0)