Some mods target the Mono version of the game, which is available by opting into the Steam beta branch "alternate"
Decompiled source of SaveFileSharingMod v1.1.2
Mods/SaveFileSharing.dll
Decompiled 5 months agousing System; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Threading.Tasks; using MelonLoader; using MelonLoader.Preferences; using SaveFileSharing; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(SaveFileSharingMod), "SaveFileSharingMod", "1.0", "Sour420", null)] [assembly: MelonColor] [assembly: MelonGame("TVGS", "Schedule I")] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("SaveFileSharing")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("SaveFileSharing")] [assembly: AssemblyTitle("SaveFileSharing")] [assembly: AssemblyVersion("1.0.0.0")] namespace SaveFileSharing; public static class BuildInfo { public const string Name = "SaveFileSharingMod"; public const string Description = "Share and get save files from your friends so you dont need one person to always be on to play!"; public const string Author = "Sour420"; public const string Company = null; public const string Version = "1.0"; public const string DownloadLink = null; } public class SaveFileSharingMod : MelonMod { private static readonly HttpClient httpClient = new HttpClient(); private static string downloadUrl; private static string uploadUrl; private static string apiKey; private static int saveSlotNumber; private static string saveFolderPath; private static string zipFilePath; public override void OnApplicationStart() { MelonPreferences_Category obj = MelonPreferences.CreateCategory("SaveFileSharingMod"); obj.SetFilePath("UserData/SaveFileSharing.cfg"); MelonPreferences_Entry<int> val = obj.CreateEntry<int>("SaveSlotNumber", 1, (string)null, (string)null, false, false, (ValueValidator)null, (string)null); MelonPreferences_Entry<string> val2 = obj.CreateEntry<string>("DownloadUrl", "https://[ID].supabase.co/storage/v1/object/public/save-sync/SaveGame_", (string)null, (string)null, false, false, (ValueValidator)null, (string)null); MelonPreferences_Entry<string> val3 = obj.CreateEntry<string>("UploadUrl", "https://[ID].supabase.co/storage/v1/object/save-sync/SaveGame_", (string)null, (string)null, false, false, (ValueValidator)null, (string)null); MelonPreferences_Entry<string> val4 = obj.CreateEntry<string>("ApiKey", "", (string)null, (string)null, false, false, (ValueValidator)null, (string)null); MelonPreferences_Entry<string> val5 = obj.CreateEntry<string>("SteamID", "", (string)null, (string)null, false, false, (ValueValidator)null, (string)null); obj.SaveToFile(true); if (string.IsNullOrEmpty(val5.Value) || !val5.Value.All(char.IsDigit)) { MelonLogger.Error("[SaveFileSharingMod] SteamID is invalid! Please make sure it only contains numbers."); return; } string path = "Saves\\" + val5.Value; string path2 = Path.Combine(Application.persistentDataPath, path); saveSlotNumber = val.Value; downloadUrl = $"{val2.Value}{saveSlotNumber}.zip"; uploadUrl = $"{val3.Value}{saveSlotNumber}.zip"; apiKey = val4.Value; saveFolderPath = Path.Combine(path2, $"SaveGame_{saveSlotNumber}"); zipFilePath = Path.Combine(path2, $"SaveGame_{saveSlotNumber}.zip"); MelonLogger.Msg($"SaveFileMod initialized for SaveGame_{saveSlotNumber}"); MelonLogger.Msg("Save Path: " + saveFolderPath); Task.Run((Func<Task?>)DownloadAndExtractSave).Wait(); } public override void OnApplicationQuit() { Task.Run((Func<Task?>)ZipAndUploadSave).Wait(); } private async Task DownloadAndExtractSave() { try { MelonLogger.Msg("Downloading save file..."); byte[] bytes = await httpClient.GetByteArrayAsync(downloadUrl); File.WriteAllBytes(zipFilePath, bytes); if (Directory.Exists(saveFolderPath)) { Directory.Delete(saveFolderPath, recursive: true); } ZipFile.ExtractToDirectory(zipFilePath, saveFolderPath); MelonLogger.Msg("Save file downloaded and extracted successfully!"); } catch (Exception ex) { MelonLogger.Warning("Downloading save file failed: " + ex.Message); } } private async Task ZipAndUploadSave() { _ = 1; try { MelonLogger.Msg("Zipping save folder..."); if (Directory.Exists(saveFolderPath)) { if (File.Exists(zipFilePath)) { File.Delete(zipFilePath); } using (ZipArchive destination = ZipFile.Open(zipFilePath, ZipArchiveMode.Create)) { string[] files = Directory.GetFiles(saveFolderPath, "*", SearchOption.AllDirectories); foreach (string text in files) { string entryName = text.Substring(saveFolderPath.Length + 1); destination.CreateEntryFromFile(text, entryName); } } MelonLogger.Msg("Zip created! Uploading to Supabase..."); byte[] content = File.ReadAllBytes(zipFilePath); HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Put, uploadUrl) { Content = new ByteArrayContent(content) }; httpRequestMessage.Headers.Add("Authorization", "Bearer " + apiKey); httpRequestMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zip"); HttpResponseMessage response = await httpClient.SendAsync(httpRequestMessage); if (response.IsSuccessStatusCode) { MelonLogger.Msg("Save file uploaded successfully to Supabase!"); return; } string value = await response.Content.ReadAsStringAsync(); MelonLogger.Warning($"Upload failed: {response.StatusCode}. Response: {value}"); } else { MelonLogger.Warning("Save folder does not exist, aborting zip operation."); } } catch (Exception ex) { MelonLogger.Warning("Upload failed: " + ex.Message); } } }