diff --git a/installer/main.py b/installer/main.py index 9454650..a4f1fef 100644 --- a/installer/main.py +++ b/installer/main.py @@ -15,8 +15,8 @@ "win32": "win", "darwin": "macos", }[sys.platform] -BEPINEX = f"733/BepInEx-Unity.IL2CPP-{OS}-x64-6.0.0-be.733%2B995f049" -POLYMOD = "https://github.com/PolyModdingTeam/PolyMod/releases/latest/download/PolyMod.dll" +BEPINEX = "https://polymod.dev/data/bepinex.txt" +POLYMOD = "https://api.github.com/repos/PolyModdingTeam/PolyMod/releases" def resource_path(path): @@ -54,6 +54,7 @@ def prepare(target): return path_entry.configure(state=customtkinter.DISABLED) browse_button.configure(state=customtkinter.DISABLED) + prerelease_checkbox.destroy() install_button.destroy() uninstall_button.destroy() progress_bar = customtkinter.CTkProgressBar(app, determinate_speed=50 / 2) @@ -65,13 +66,16 @@ def prepare(target): def install(path): to_zip( requests.get( - f"https://builds.bepinex.dev/projects/bepinex_be/{BEPINEX}.zip" + requests.get(BEPINEX).text.strip().replace("{os}", OS) ) ).extractall(path) progress_bar.step() + for release in requests.get(POLYMOD).json(): + if release["prerelease"] and not prerelease_checkbox.get(): continue + latest = release open(path + "/BepInEx/plugins/PolyMod.dll", "wb").write( - requests.get(POLYMOD).content + requests.get(latest["assets"][0]["browser_download_url"]).content ) progress_bar.step() @@ -133,6 +137,8 @@ def quit(): app, placeholder_text="Game path", width=228) browse_button = customtkinter.CTkButton( app, text="Browse", command=browse, width=1) +prerelease_checkbox = customtkinter.CTkCheckBox( + app, text="Prerelease", width=1) install_button = customtkinter.CTkButton( app, text="Install", command=lambda: prepare(install)) uninstall_button = customtkinter.CTkButton( @@ -140,7 +146,8 @@ def quit(): path_entry.grid(column=0, row=0, padx=5, pady=5) browse_button.grid(column=1, row=0, padx=(0, 5), pady=5) -install_button.grid(column=0, row=1, columnspan=2, padx=5, pady=5) -uninstall_button.grid(column=0, row=2, columnspan=2, padx=5, pady=5) +prerelease_checkbox.grid(column=0, row=1, columnspan=2, padx=5, pady=5) +install_button.grid(column=0, row=2, columnspan=2, padx=5, pady=5) +uninstall_button.grid(column=0, row=3, columnspan=2, padx=5, pady=5) app.mainloop() diff --git a/resources/localization.json b/resources/localization.json index dfc7f03..efc9a56 100644 --- a/resources/localization.json +++ b/resources/localization.json @@ -161,6 +161,26 @@ "Elyrion": "πȱ∫ỹmȱδ ƒƒƒƒƒƒƒ ŋȱŧ ȱrrȱ #₺rr∑ŋŧ ƒƒƒƒƒƒƒ ỹ maỹ ŋȱŧ ~ȱr§ #ȱrr∑#ŧ∫ỹ!", "German (Germany)": "Diese Version von PolyMod ist nicht für die aktuelle Version der Anwendung ausgelegt und könnte nicht funktionieren!" }, + "polymod_debug": { + "English": "Debug", + "Russian": "Дебаг" + }, + "polymod_autoupdate": { + "English": "Auto-update", + "Russian": "Автообновление" + }, + "polymod_autoupdate_alpha": { + "English": "Include alphas", + "Russian": "Include alphas" + }, + "polymod_autoupdate_description": { + "English": "New update available!", + "Russian": "Доступно новое обновление!" + }, + "polymod_autoupdate_update": { + "English": "Update", + "Russian": "Обновить" + }, "polymod_hub_config": { "English": "CONFIG", "Russian": "КОНФИГ" diff --git a/src/Managers/AutoUpdate.cs b/src/Managers/AutoUpdate.cs new file mode 100644 index 0000000..ef7a043 --- /dev/null +++ b/src/Managers/AutoUpdate.cs @@ -0,0 +1,162 @@ +using System.Diagnostics; +using System.IO.Compression; +using System.Text.Json; +using HarmonyLib; +using UnityEngine; + +namespace PolyMod.Managers; + +internal static class AutoUpdate +{ + [HarmonyPostfix] + [HarmonyPatch(typeof(StartScreen), nameof(StartScreen.Start))] + private static void StartScreen_Start() + { + if (!Plugin.config.autoUpdate) return; + if (Environment.GetEnvironmentVariable("WINEPREFIX") != null) + { + Plugin.logger.LogError("Autoupdate is not supported on Wine!"); + return; + } + HttpClient client = new(); + client.DefaultRequestHeaders.Add("User-Agent", "PolyMod"); + try + { + var json = JsonDocument.Parse( + client.GetAsync("https://api.github.com/repos/PolyModdingTeam/PolyMod/releases").UnwrapAsync() + .Content.ReadAsStringAsync().UnwrapAsync() + ); + JsonElement? latest = null; + for (int i = 0; i < json.RootElement.GetArrayLength(); i++) + { + var release = json.RootElement[i]; + if (release.GetProperty("prerelease").GetBoolean() && !Plugin.config.updatePrerelease) continue; + latest = release; + break; + } + string newVersion = latest?.GetProperty("tag_name").GetString()!.TrimStart('v')!; + if (newVersion.IsVersionOlderOrEqual(Plugin.VERSION)) return; + string os = Application.platform switch + { + RuntimePlatform.WindowsPlayer => "win", + RuntimePlatform.LinuxPlayer => "linux", + RuntimePlatform.OSXPlayer => "macos", + _ => "unknown", + }; + if (os == "unknown") + { + Plugin.logger.LogError("Unsupported platform for autoupdate!"); + return; + } + string bepinex_url = client + .GetAsync("https://polymod.dev/data/bepinex.txt").UnwrapAsync() + .Content.ReadAsStringAsync().UnwrapAsync() + .Replace("{os}", os); + void Update() + { + Time.timeScale = 0; + File.WriteAllBytes( + Path.Combine(Plugin.BASE_PATH, "PolyMod.new.dll"), + client.GetAsync(latest?.GetProperty("assets")[0].GetProperty("browser_download_url").GetString()!).UnwrapAsync() + .Content.ReadAsByteArrayAsync().UnwrapAsync() + ); + using ZipArchive bepinex = new(client.GetAsync(bepinex_url).UnwrapAsync().Content.ReadAsStream()); + bepinex.ExtractToDirectory(Path.Combine(Plugin.BASE_PATH, "New"), overwriteFiles: true); + ProcessStartInfo info = new() + { + WorkingDirectory = Path.Combine(Plugin.BASE_PATH), + CreateNoWindow = true, + }; + if (Application.platform == RuntimePlatform.WindowsPlayer) + { + string batchPath = Path.Combine(Plugin.BASE_PATH, "update.bat"); + File.WriteAllText(batchPath, $@" + @echo off + echo Waiting for Polytopia.exe to exit... + :waitloop + tasklist | findstr /I ""Polytopia.exe"" >nul + if not errorlevel 1 ( + timeout /T 1 >nul + goto waitloop + ) + + echo Updating... + robocopy ""New"" . /E /MOVE /NFL /NDL /NJH /NJS /NP >nul + rmdir /S /Q ""New"" + del /F /Q ""BepInEx\plugins\PolyMod.dll"" + move /Y ""PolyMod.new.dll"" ""BepInEx\plugins\PolyMod.dll"" + + echo Launching game... + start steam://rungameid/874390 + timeout /T 3 /NOBREAK >nul + exit + "); + info.FileName = "cmd.exe"; + info.Arguments = $"/C start \"\" \"{batchPath}\""; + info.WorkingDirectory = Plugin.BASE_PATH; + info.CreateNoWindow = true; + info.UseShellExecute = false; + } + if (Application.platform == RuntimePlatform.LinuxPlayer || Application.platform == RuntimePlatform.OSXPlayer) + { + string bashPath = Path.Combine(Plugin.BASE_PATH, "update.sh"); + File.WriteAllText(bashPath, $@" + #!/bin/bash + + echo ""Waiting for Polytopia to exit..."" + while pgrep -x ""Polytopia"" > /dev/null; do + sleep 1 + done + + echo ""Updating..."" + mv New/* . && rm -rf New + rm -f BepInEx/plugins/PolyMod.dll + mv -f PolyMod.new.dll BepInEx/plugins/PolyMod.dll + + echo ""Launching game..."" + xdg-open steam://rungameid/874390 & + + sleep 3 + exit 0 + "); + + System.Diagnostics.Process chmod = new System.Diagnostics.Process(); + chmod.StartInfo.FileName = "chmod"; + chmod.StartInfo.Arguments = $"+x \"{bashPath}\""; + chmod.StartInfo.UseShellExecute = false; + chmod.StartInfo.CreateNoWindow = true; + chmod.Start(); + chmod.WaitForExit(); + + info.FileName = "/bin/bash"; + info.Arguments = $"\"{bashPath}\""; + info.WorkingDirectory = Plugin.BASE_PATH; + info.CreateNoWindow = true; + info.UseShellExecute = false; + } + Process.Start(info); + Application.Quit(); + } + PopupManager.GetBasicPopup(new( + Localization.Get("polymod.autoupdate"), + Localization.Get("polymod.autoupdate.description"), + new(new PopupBase.PopupButtonData[] { + new( + "polymod.autoupdate.update", + PopupBase.PopupButtonData.States.None, + (Il2CppSystem.Action)Update + ) + })) + ).Show(); + } + catch (Exception e) + { + Plugin.logger.LogError($"Failed to check updates: {e.Message}"); + } + } + + internal static void Init() + { + Harmony.CreateAndPatchAll(typeof(AutoUpdate)); + } +} \ No newline at end of file diff --git a/src/Managers/Hub.cs b/src/Managers/Hub.cs index ff8abc1..43b186f 100644 --- a/src/Managers/Hub.cs +++ b/src/Managers/Hub.cs @@ -298,6 +298,7 @@ internal static void ShowConfigPopup() polymodPopup.Description = ""; polymodPopup.buttonData = CreateConfigPopupButtonData(); + polymodPopup.ShowSetWidth(POPUP_WIDTH); polymodPopup.Show(); } @@ -315,41 +316,62 @@ internal static PopupButtonData[] CreateConfigPopupButtonData() else { string debugButtonName = Localization.Get( - "polymod.hub.config.enable", - new Il2CppSystem.Object[] { "DEBUG" } + Plugin.config.debug ? "polymod.hub.config.disable" : "polymod.hub.config.enable", + new Il2CppSystem.Object[] { Localization.Get("polymod.debug", + new Il2CppSystem.Object[]{}).ToUpperInvariant() } + ); + string autoUpdateButtonName = Localization.Get( + Plugin.config.autoUpdate ? "polymod.hub.config.disable" : "polymod.hub.config.enable", + new Il2CppSystem.Object[] { Localization.Get("polymod.autoupdate", + new Il2CppSystem.Object[]{}).ToUpperInvariant() } + ); + string includeAlphasButtonName = Localization.Get( + Plugin.config.updatePrerelease ? "polymod.hub.config.disable" : "polymod.hub.config.enable", + new Il2CppSystem.Object[] { Localization.Get("polymod.autoupdate.alpha", + new Il2CppSystem.Object[]{}).ToUpperInvariant() } ); - if (Plugin.config.debug) - { - debugButtonName = Localization.Get( - "polymod.hub.config.disable", - new Il2CppSystem.Object[] { "DEBUG" } - ); - } popupButtons.Add(new PopupButtonData(debugButtonName, PopupButtonData.States.None, (UIButtonBase.ButtonAction)OnDebugButtonClicked, -1, true, null)); - //popupButtons.Add(new PopupButtonData("", PopupButtonData.States.None, (UIButtonBase.ButtonAction)OnAutoUpdateButtonClicked, -1, true, null)); - //popupButtons.Add(new PopupButtonData("", PopupButtonData.States.Disabled, (UIButtonBase.ButtonAction)OnIncludeAlphasButtonClicked, -1, true, null)); + popupButtons.Add(new PopupButtonData(autoUpdateButtonName, PopupButtonData.States.None, (UIButtonBase.ButtonAction)OnAutoUpdateButtonClicked, -1, true, null)); + popupButtons.Add(new PopupButtonData(includeAlphasButtonName, Plugin.config.autoUpdate ? PopupButtonData.States.None : PopupButtonData.States.Disabled, (UIButtonBase.ButtonAction)OnIncludeAlphasButtonClicked, -1, true, null)); } return popupButtons.ToArray(); void OnDebugButtonClicked(int buttonId, BaseEventData eventData) { - Plugin.config = new(debug: !Plugin.config.debug); + Plugin.config = new(debug: !Plugin.config.debug, autoUpdate: Plugin.config.autoUpdate, updatePrerelease: Plugin.config.updatePrerelease); Plugin.WriteConfig(); Plugin.UpdateConsole(); NotificationManager.Notify(Localization.Get( "polymod.config.setto", - new Il2CppSystem.Object[] { "Debug", Plugin.config.debug } + new Il2CppSystem.Object[] { Localization.Get("polymod.debug", + new Il2CppSystem.Object[]{}), Plugin.config.debug } )); isConfigPopupActive = false; } void OnAutoUpdateButtonClicked(int buttonId, BaseEventData eventData) { + Plugin.config = new(debug: Plugin.config.debug, autoUpdate: !Plugin.config.autoUpdate, updatePrerelease: Plugin.config.updatePrerelease); + Plugin.WriteConfig(); + Plugin.UpdateConsole(); + NotificationManager.Notify(Localization.Get( + "polymod.config.setto", + new Il2CppSystem.Object[] { Localization.Get("polymod.autoupdate", + new Il2CppSystem.Object[]{}), Plugin.config.autoUpdate } + )); isConfigPopupActive = false; } void OnIncludeAlphasButtonClicked(int buttonId, BaseEventData eventData) { + Plugin.config = new(debug: Plugin.config.debug, autoUpdate: Plugin.config.autoUpdate, updatePrerelease: !Plugin.config.updatePrerelease); + Plugin.WriteConfig(); + Plugin.UpdateConsole(); + NotificationManager.Notify(Localization.Get( + "polymod.config.setto", + new Il2CppSystem.Object[] { Localization.Get("polymod.autoupdate.alpha", + new Il2CppSystem.Object[]{}), Plugin.config.updatePrerelease } + )); isConfigPopupActive = false; } diff --git a/src/Plugin.cs b/src/Plugin.cs index c09811d..025258c 100644 --- a/src/Plugin.cs +++ b/src/Plugin.cs @@ -12,7 +12,9 @@ namespace PolyMod; public partial class Plugin : BepInEx.Unity.IL2CPP.BasePlugin { internal record PolyConfig( - bool debug = false + bool debug = false, + bool autoUpdate = true, + bool updatePrerelease = false ); internal const int AUTOIDX_STARTS_FROM = 1000; @@ -49,8 +51,8 @@ public override void Load() catch { config = new(); - WriteConfig(); } + WriteConfig(); UpdateConsole(); logger = Log; ConfigFile.CoreConfig[new("Logging.Disk", "WriteUnityLog")].BoxedValue = true; @@ -58,6 +60,7 @@ public override void Load() Compatibility.Init(); Audio.Init(); + AutoUpdate.Init(); Loc.Init(); Visual.Init(); Hub.Init(); diff --git a/src/Util.cs b/src/Util.cs index fad25af..4d2fce2 100644 --- a/src/Util.cs +++ b/src/Util.cs @@ -6,7 +6,6 @@ using Polytopia.Data; namespace PolyMod; - internal static class Util { internal static Il2CppSystem.Type WrapType() where T : class @@ -31,11 +30,33 @@ internal static Version Cast(this Il2CppSystem.Version self) return new(self.ToString()); } + internal static T UnwrapAsync(this Task self) + { + return self.GetAwaiter().GetResult(); + } + internal static Version CutRevision(this Version self) { return new(self.Major, self.Minor, self.Build); } + internal static bool IsVersionOlderOrEqual(this string version1, string version2) + { + Version version1_ = new(version1.Split('-')[0]); + Version version2_ = new(version2.Split('-')[0]); + + if (version1_ < version2_) return true; + if (version1_ > version2_) return false; + + string pre1 = version1.Contains('-') ? version1.Split('-')[1] : ""; + string pre2 = version2.Contains('-') ? version2.Split('-')[1] : ""; + + if (string.IsNullOrEmpty(pre1) && !string.IsNullOrEmpty(pre2)) return false; + if (!string.IsNullOrEmpty(pre1) && string.IsNullOrEmpty(pre2)) return true; + + return string.Compare(pre1, pre2, StringComparison.Ordinal) <= 0; + } + internal static string GetStyle(TribeData.Type tribe, SkinType skin) { return skin != SkinType.Default ? EnumCache.GetName(skin) : EnumCache.GetName(tribe);