From 9cc3f16ac45eb21e93b4a4ee6fc2a529829c478b Mon Sep 17 00:00:00 2001 From: doombubbles Date: Thu, 22 Dec 2022 10:04:15 -0800 Subject: [PATCH] Preliminary fix for 1330 studios ML version --- BloonsTD6 Mod Helper/Api/AutoSave.cs | 90 ------------------- BloonsTD6 Mod Helper/Api/ProfileManagement.cs | 6 +- BloonsTD6 Mod Helper/MelonMain.Settings.cs | 44 --------- BloonsTD6 Mod Helper/MelonMain.cs | 10 +-- .../Patches/InitialLoadTasks_MoveNext.cs | 4 +- .../IsModdedClientPatches.cs | 2 +- .../ModdedClientBypassing.cs | 37 -------- .../ModdedClientUsagePatches.cs | 50 ----------- .../Resources/LoadGameModelAsync_MoveNext.cs | 4 +- .../LocalizationManger_LoadTableAsync.cs | 4 +- .../Patches/UI/LayoutGroupPatches.cs | 4 +- .../Patches/UI/TitleScreen_Start.cs | 20 ++++- .../UI/Menus/ModSettingsMenu.cs | 19 ++-- BloonsTD6 Mod Helper/UI/Menus/ModsMenu.cs | 6 +- Shared/Api/Helpers/FileIOHelper.cs | 7 +- Shared/ModHelper.cs | 2 +- 16 files changed, 51 insertions(+), 258 deletions(-) delete mode 100644 BloonsTD6 Mod Helper/Api/AutoSave.cs delete mode 100644 BloonsTD6 Mod Helper/Patches/ModdedClientChecking/ModdedClientBypassing.cs delete mode 100644 BloonsTD6 Mod Helper/Patches/ModdedClientChecking/ModdedClientUsagePatches.cs diff --git a/BloonsTD6 Mod Helper/Api/AutoSave.cs b/BloonsTD6 Mod Helper/Api/AutoSave.cs deleted file mode 100644 index 71534bf2f..000000000 --- a/BloonsTD6 Mod Helper/Api/AutoSave.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Diagnostics; -using System.IO; -using Assets.Scripts.Unity; -using Assets.Scripts.Unity.UI_New.Popups; -using static BTD_Mod_Helper.MelonMain; - -namespace BTD_Mod_Helper.Api; - -/// -/// Implements the features of the AutoSave mod -/// -public static class AutoSave -{ - internal static BackupCreator backup; - - internal static bool autosaveInit; - // internal static string profileSaveDir; - - internal static void InitAutosave(string settingsDir) - { - if (autosaveInit) - return; - - ModHelper.Log("Starting to initiate profile AutoSaving..."); - InitAutosaveSettings(settingsDir); - backup = new BackupCreator(AutosavePath, MaxSavedBackups); - ScheduleAutosave(); - autosaveInit = true; - - ModHelper.Log("Successfully initiated profile AutoSaving"); - } - - private static void InitAutosaveSettings(string settingsDir) - { - if (string.IsNullOrEmpty(AutosavePath)) - { - var autosaveDir = settingsDir + "\\Autosave"; - Directory.CreateDirectory(autosaveDir); - AutosavePath.SetValue(autosaveDir); - AutosavePath.defaultValue = autosaveDir; - } - } - - private static void ScheduleAutosave() - { - const int secondsPerMinute = 60; - TaskScheduler.ScheduleTask(() => - { - backup.CreateBackup(); - ScheduleAutosave(); - }, - Api.Enums.ScheduleType.WaitForSeconds, TimeBetweenBackup * secondsPerMinute); - } - - internal static void OpenAutoSaveDir() - { - var saveDirectory = Game.instance.GetSaveDirectory(); - if (string.IsNullOrEmpty(saveDirectory) || !Directory.Exists(saveDirectory)) - { - PopupScreen.instance.SafelyQueue(screen => - screen.ShowOkPopup("Can't open Save directory because it wasn't found")); - } - else - { - Process.Start(saveDirectory); - } - } - - internal static void OpenBackupDir() - { - if (string.IsNullOrEmpty(AutosavePath) || !Directory.Exists(AutosavePath)) - { - PopupScreen.instance.SafelyQueue(screen => - screen.ShowOkPopup("Can't open Backup directory because it wasn't found")); - } - else - { - Process.Start(AutosavePath); - } - } - - internal static void SetAutosaveDirectory(string newPath) - { - if (!string.IsNullOrEmpty(newPath)) - { - Directory.CreateDirectory(newPath); - backup.MoveBackupDir(newPath); - } - } -} \ No newline at end of file diff --git a/BloonsTD6 Mod Helper/Api/ProfileManagement.cs b/BloonsTD6 Mod Helper/Api/ProfileManagement.cs index 80aacb888..0439a7327 100644 --- a/BloonsTD6 Mod Helper/Api/ProfileManagement.cs +++ b/BloonsTD6 Mod Helper/Api/ProfileManagement.cs @@ -232,10 +232,10 @@ internal static void UnCleanProfile(ProfileModel profile) { if (profile.savedMaps?.ContainsKey(map) == true) { - var mapSaveDataModel = profile.savedMaps[map]; + var mapSaveDataModel = profile.savedMaps[(string) map]; if (mapSaveDataModel.players.ContainsKey(player)) { - mapSaveDataModel.players[player].hero = hero; + mapSaveDataModel.players[(int) player].hero = hero; } } } @@ -293,7 +293,7 @@ private static void CleanDictionary(Il2CppSystem.Collections.Generic.Dictiona { return; } - + foreach (var (thing, value) in dictionary) { if (clean(thing)) diff --git a/BloonsTD6 Mod Helper/MelonMain.Settings.cs b/BloonsTD6 Mod Helper/MelonMain.Settings.cs index d54573b2f..096562c8e 100644 --- a/BloonsTD6 Mod Helper/MelonMain.Settings.cs +++ b/BloonsTD6 Mod Helper/MelonMain.Settings.cs @@ -188,50 +188,6 @@ internal partial class MelonMain onSave = ModHelperFiles.CreateTargetsFile }; - #region Autosave - - public static readonly ModSettingCategory AutoSaveCategory = new("Auto Save Settings") - { - icon = SaveGameIcon - }; - - public static readonly ModSettingButton OpenBackupDir = new(AutoSave.OpenBackupDir) - { - displayName = "Open Backup Directory", - buttonText = "Open", - category = AutoSaveCategory - }; - - public static readonly ModSettingButton OpenSaveDir = new(AutoSave.OpenAutoSaveDir) - { - displayName = "Open Save Directory", - buttonText = "Open", - category = AutoSaveCategory - }; - - public static readonly ModSettingFolder AutosavePath = - new(Path.Combine(ModHelper.ModHelperDirectory, "Mod Settings")) - { - displayName = "Backup Directory", - onSave = AutoSave.SetAutosaveDirectory, - category = AutoSaveCategory - }; - - public static readonly ModSettingInt TimeBetweenBackup = new(30) - { - displayName = "Minutes Between Each Backup", - category = AutoSaveCategory - }; - - public static readonly ModSettingInt MaxSavedBackups = new(10) - { - displayName = "Max Saved Backups", - onSave = max => AutoSave.backup.SetMaxBackups(max), - category = AutoSaveCategory - }; - - #endregion - #region Debug private static readonly ModSettingCategory Debug = new("Debug"); diff --git a/BloonsTD6 Mod Helper/MelonMain.cs b/BloonsTD6 Mod Helper/MelonMain.cs index b136a5495..0e8cb0607 100644 --- a/BloonsTD6 Mod Helper/MelonMain.cs +++ b/BloonsTD6 Mod Helper/MelonMain.cs @@ -76,7 +76,7 @@ public override void OnInitialize() public override void OnUpdate() { ModByteLoader.OnUpdate(); - InitialLoadTasks_MoveNext.Update(); + // InitialLoadTasks_MoveNext.Update(); if (Game.instance is null) return; @@ -112,8 +112,6 @@ public override void OnTitleScreen() if (!scheduledInGamePatch) Schedule_InGame_Loaded(); - AutoSave.InitAutosave(this.GetModSettingsDir(true)); - foreach (var gameMode in Game.instance.model.mods) { if (gameMode.name.EndsWith("Only")) @@ -171,10 +169,4 @@ public override void OnMainMenu() Fonts.Load(); RoundSetChanger.EnsureHidden(); } - - #region Autosave - - public override void OnMatchEnd() => AutoSave.backup.CreateBackup(); - - #endregion } \ No newline at end of file diff --git a/BloonsTD6 Mod Helper/Patches/InitialLoadTasks_MoveNext.cs b/BloonsTD6 Mod Helper/Patches/InitialLoadTasks_MoveNext.cs index 2ca0c4c33..f6ddd8847 100644 --- a/BloonsTD6 Mod Helper/Patches/InitialLoadTasks_MoveNext.cs +++ b/BloonsTD6 Mod Helper/Patches/InitialLoadTasks_MoveNext.cs @@ -15,7 +15,7 @@ using Object = Il2CppSystem.Object; namespace BTD_Mod_Helper.Patches; -[HarmonyPatch(typeof(Main._InitialLoadTasks_d__45), nameof(Main._InitialLoadTasks_d__45.MoveNext))] +/*[HarmonyPatch(typeof(Main._InitialLoadTasks_d__45), nameof(Main._InitialLoadTasks_d__45.MoveNext))] internal static class InitialLoadTasks_MoveNext { internal static List modsTasks; @@ -150,4 +150,4 @@ private static bool Prefix(ref Task __result) } return true; } -} \ No newline at end of file +}*/ \ No newline at end of file diff --git a/BloonsTD6 Mod Helper/Patches/ModdedClientChecking/IsModdedClientPatches.cs b/BloonsTD6 Mod Helper/Patches/ModdedClientChecking/IsModdedClientPatches.cs index da1027a37..4abee0c70 100644 --- a/BloonsTD6 Mod Helper/Patches/ModdedClientChecking/IsModdedClientPatches.cs +++ b/BloonsTD6 Mod Helper/Patches/ModdedClientChecking/IsModdedClientPatches.cs @@ -16,7 +16,7 @@ private static IEnumerable TargetMethods() [HarmonyPrefix] private static bool Prefix(ref bool __result) { - if (ModdedClientBypassing.CurrentlyBypassingCheck && MelonMain.BypassSavingRestrictions) + if (MelonMain.BypassSavingRestrictions) { __result = false; return false; diff --git a/BloonsTD6 Mod Helper/Patches/ModdedClientChecking/ModdedClientBypassing.cs b/BloonsTD6 Mod Helper/Patches/ModdedClientChecking/ModdedClientBypassing.cs deleted file mode 100644 index 4d9ca1ffe..000000000 --- a/BloonsTD6 Mod Helper/Patches/ModdedClientChecking/ModdedClientBypassing.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Assets.Scripts.Unity; - -namespace BTD_Mod_Helper.Patches.ModdedClientChecking; - -/// -/// You forced our hand :( -/// -internal class ModdedClientBypassing -{ - /// - /// The nuclear option would be just setting this to true, which would entirely bypass all of NK's clientside checks - /// - private const bool DefaultBypassCheck = false; - - /// - /// Whether the ModdedClient check is currently being bypassed - /// - public static bool CurrentlyBypassingCheck { get; private set; } - - /// - /// Called in prefix patches on methods where we think modded clients should be accepted - /// - internal static void StartBypassingCheck() - { - CurrentlyBypassingCheck = true; - Modding.isModdedClient = false; - } - - /// - /// Called in postfix patches on methods where we think modded clients should be accepted - /// - internal static void StopBypassingCheck() - { - CurrentlyBypassingCheck = DefaultBypassCheck; - Modding.isModdedClient = !DefaultBypassCheck; - } -} \ No newline at end of file diff --git a/BloonsTD6 Mod Helper/Patches/ModdedClientChecking/ModdedClientUsagePatches.cs b/BloonsTD6 Mod Helper/Patches/ModdedClientChecking/ModdedClientUsagePatches.cs deleted file mode 100644 index b8b495561..000000000 --- a/BloonsTD6 Mod Helper/Patches/ModdedClientChecking/ModdedClientUsagePatches.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Collections.Generic; -using System.Reflection; -using Assets.Scripts.Unity.Player; -using Assets.Scripts.Unity.UI_New.DailyChallenge; -using Assets.Scripts.Unity.UI_New.GameOver; -using Assets.Scripts.Unity.UI_New.InGame; -using Assets.Scripts.Utils; -using BTD_Mod_Helper.Api; - -namespace BTD_Mod_Helper.Patches.ModdedClientChecking; - -[HarmonyPatch] -internal static class ModdedClientUsagePatches -{ - private static IEnumerable TargetMethods() - { - yield return AccessTools.Method(typeof(Btd6Player), nameof(Btd6Player.Save)); - yield return AccessTools.Method(typeof(Btd6Player), nameof(Btd6Player.SaveNow)); - yield return AccessTools.Method(typeof(Btd6Player), nameof(Btd6Player.SyncNow)); - yield return AccessTools.Method(typeof(InGame), nameof(InGame.Continue)); - yield return AccessTools.Method(typeof(InGame), nameof(InGame.ContinueFromCheckpoint)); - yield return AccessTools.Method(typeof(InGame), nameof(InGame.CreateMapSave)); - yield return AccessTools.Method(typeof(InGame), nameof(InGame.RoundEnd)); - yield return AccessTools.Method(typeof(InGame), nameof(InGame.OnVictory)); - yield return AccessTools.Method(typeof(OnlineProfileUpdater), nameof(OnlineProfileUpdater.LateUpdate)); - - if (MoreAccessTools.TryGetNestedClassMethod(typeof(OnlineProfileManager), "Upload", "MoveNext", out var m)) - yield return m; - if (MoreAccessTools.TryGetNestedClassMethod(typeof(Btd6Player), "LoadOnlineData", "MoveNext", out m)) - yield return m; - if (MoreAccessTools.TryGetNestedClassMethod(typeof(BossVictoryScreen), "Open", "MoveNext", out m)) - yield return m; - if (MoreAccessTools.TryGetNestedClassMethod(typeof(BossEventScreenPlayPanel), "Open", "MoveNext", out m)) - yield return m; - if (MoreAccessTools.TryGetNestedClassMethod(typeof(BossEventScreen), "Open", "MoveNext", out m)) - yield return m; - } - - [HarmonyPrefix] - private static void Prefix() - { - ModdedClientBypassing.StartBypassingCheck(); - } - - [HarmonyPostfix] - private static void Postfix() - { - ModdedClientBypassing.StopBypassingCheck(); - } -} \ No newline at end of file diff --git a/BloonsTD6 Mod Helper/Patches/Resources/LoadGameModelAsync_MoveNext.cs b/BloonsTD6 Mod Helper/Patches/Resources/LoadGameModelAsync_MoveNext.cs index 1dd044271..e0407003b 100644 --- a/BloonsTD6 Mod Helper/Patches/Resources/LoadGameModelAsync_MoveNext.cs +++ b/BloonsTD6 Mod Helper/Patches/Resources/LoadGameModelAsync_MoveNext.cs @@ -5,7 +5,7 @@ namespace BTD_Mod_Helper.Patches.Resources; /// /// This makes the first GameModel loaded always be the one returned, even if another GameModel load tries to happen /// -[HarmonyPatch(typeof(GameModelUtil._LoadGameModelAsync_d__9), +/*[HarmonyPatch(typeof(GameModelUtil._LoadGameModelAsync_d__9), nameof(GameModelUtil._LoadGameModelAsync_d__9.MoveNext))] internal static class LoadGameModelAsync_MoveNext { @@ -32,4 +32,4 @@ private static void Postfix(GameModelUtil._LoadGameModelAsync_d__9 __instance) } } -} \ No newline at end of file +}*/ \ No newline at end of file diff --git a/BloonsTD6 Mod Helper/Patches/Resources/LocalizationManger_LoadTableAsync.cs b/BloonsTD6 Mod Helper/Patches/Resources/LocalizationManger_LoadTableAsync.cs index 11e89cd37..153d40ed3 100644 --- a/BloonsTD6 Mod Helper/Patches/Resources/LocalizationManger_LoadTableAsync.cs +++ b/BloonsTD6 Mod Helper/Patches/Resources/LocalizationManger_LoadTableAsync.cs @@ -5,7 +5,7 @@ namespace BTD_Mod_Helper.Patches.Resources; -[HarmonyPatch(typeof(LocalizationManager._LoadTableAsync_d__45), +/*[HarmonyPatch(typeof(LocalizationManager._LoadTableAsync_d__45), nameof(LocalizationManager._LoadTableAsync_d__45.MoveNext))] internal static class LocalizationManger_LoadTableAsync { @@ -29,4 +29,4 @@ private static void Postfix(LocalizationManager._LoadTableAsync_d__45 __instance } } } -} \ No newline at end of file +}*/ \ No newline at end of file diff --git a/BloonsTD6 Mod Helper/Patches/UI/LayoutGroupPatches.cs b/BloonsTD6 Mod Helper/Patches/UI/LayoutGroupPatches.cs index 55c51ff4e..029af05a6 100644 --- a/BloonsTD6 Mod Helper/Patches/UI/LayoutGroupPatches.cs +++ b/BloonsTD6 Mod Helper/Patches/UI/LayoutGroupPatches.cs @@ -6,7 +6,7 @@ namespace BTD_Mod_Helper.Patches.UI; /// /// I'm just annoyed that Unity doesn't work this way by default lol /// -internal static class LayoutGroupPatches +/*internal static class LayoutGroupPatches { [HarmonyPatch(typeof(LayoutGroup), nameof(LayoutGroup.flexibleHeight), MethodType.Getter)] internal static class LayoutGroup_FlexibleHeight @@ -109,4 +109,4 @@ private static void Postfix(HorizontalLayoutGroup __instance, ref float __result } } } -} \ No newline at end of file +}*/ \ No newline at end of file diff --git a/BloonsTD6 Mod Helper/Patches/UI/TitleScreen_Start.cs b/BloonsTD6 Mod Helper/Patches/UI/TitleScreen_Start.cs index 63f7a49ed..e56101e8d 100644 --- a/BloonsTD6 Mod Helper/Patches/UI/TitleScreen_Start.cs +++ b/BloonsTD6 Mod Helper/Patches/UI/TitleScreen_Start.cs @@ -1,6 +1,8 @@ -using System.Linq; +using System; +using System.Linq; using Assets.Main.Scenes; using BTD_Mod_Helper.Api; +using NinjaKiwi.Common; namespace BTD_Mod_Helper.Patches.UI; @@ -26,6 +28,22 @@ internal static void Postfix() ModContent.GetContent().Do(task => task.RunSync()); } + var currentTable = LocalizationManager.Instance.textTable; + var defaultTable = LocalizationManager.Instance.defaultTable; + foreach (var namedModContent in ModContent.GetContent()) + { + try + { + namedModContent.RegisterText(currentTable); + namedModContent.RegisterText(defaultTable); + } + catch (Exception e) + { + ModHelper.Log($"Failed to register text for {namedModContent}"); + ModHelper.Error(e); + } + } + ModHelper.PerformHook(mod => mod.OnTitleScreen()); } } \ No newline at end of file diff --git a/BloonsTD6 Mod Helper/UI/Menus/ModSettingsMenu.cs b/BloonsTD6 Mod Helper/UI/Menus/ModSettingsMenu.cs index 0099b6374..3e9556db4 100644 --- a/BloonsTD6 Mod Helper/UI/Menus/ModSettingsMenu.cs +++ b/BloonsTD6 Mod Helper/UI/Menus/ModSettingsMenu.cs @@ -5,14 +5,19 @@ using BTD_Mod_Helper.Api.Components; using BTD_Mod_Helper.Api.ModMenu; using BTD_Mod_Helper.Api.ModOptions; +using Il2CppSystem; +using MelonLoader.TinyJSON; +using Newtonsoft.Json; using UnityEngine; +using Console = System.Console; +using Exception = System.Exception; using Object = Il2CppSystem.Object; namespace BTD_Mod_Helper.UI.Menus; internal class ModSettingsMenu : ModGameMenu { - private BloonsMod bloonsMod; + public static BloonsMod BloonsMod { get; private set; } private Animator animator; @@ -25,8 +30,7 @@ public override bool OnMenuOpened(Object data) var gameObject = GameMenu.gameObject; gameObject.DestroyAllChildren(); - bloonsMod = ModHelper.Mods.First(m => m.IDPrefix == data?.ToString()); - CommonForegroundHeader.SetText(bloonsMod.Info.Name); + CommonForegroundHeader.SetText(BloonsMod.Info.Name); scrollPanel = gameObject.AddModHelperScrollPanel(new Info("ScrollPanel", InfoPreset.FillParent), RectTransform.Axis.Vertical, null, 150, 300); @@ -45,7 +49,7 @@ public override bool OnMenuOpened(Object data) public IEnumerator CreateMenuContent() { - foreach (var (category, modSettings) in bloonsMod.ModSettings.Values + foreach (var (category, modSettings) in BloonsMod.ModSettings.Values .GroupBy(setting => setting.category) .OrderBy(kvp => kvp.Key?.order ?? 0)) { @@ -97,8 +101,8 @@ public override void OnMenuUpdate() public override void OnMenuClosed() { animator.Play("PopupSlideOut"); - ModSettingsHandler.SaveModSettings(bloonsMod); - if (bloonsMod is MelonMain && !ModHelper.IsNet6) + ModSettingsHandler.SaveModSettings(BloonsMod); + if (BloonsMod is MelonMain && !ModHelper.IsNet6) { ModHelperHttp.UpdateSettings(); } @@ -106,6 +110,7 @@ public override void OnMenuClosed() public static void Open(BloonsMod bloonsMod) { - ModGameMenu.Open(bloonsMod.IDPrefix); + BloonsMod = bloonsMod; + ModGameMenu.Open(); } } \ No newline at end of file diff --git a/BloonsTD6 Mod Helper/UI/Menus/ModsMenu.cs b/BloonsTD6 Mod Helper/UI/Menus/ModsMenu.cs index b4aedfb0b..b43c1817c 100644 --- a/BloonsTD6 Mod Helper/UI/Menus/ModsMenu.cs +++ b/BloonsTD6 Mod Helper/UI/Menus/ModsMenu.cs @@ -307,6 +307,7 @@ private static void CreateLeftMenu(ModHelperPanel modsMenu) topRow.AddComponent(); topRow.Mask.showMaskGraphic = false; topRow.Mask.enabled = false; + topRow.ScrollContent.RectTransform.pivot = new Vector2(0, 0.5f); topRow.ScrollContent.AddDropdown(new Info("ModFilter", 500f, ModNameHeight), SortOptions.ToIl2CppList(), ModNameHeight * 4, @@ -427,11 +428,6 @@ private static void Refresh() var anyModsNeedUpdates = modPanels.Any(pair => pair.Key.UpdateAvailable && pair.Value is not null && pair.Value.gameObject.active); updateAllButton.gameObject.SetActive(anyModsNeedUpdates); - if (topRow.ScrollContent.enabled != anyModsNeedUpdates) - { - topRow.ScrollRect.enabled = anyModsNeedUpdates; - topRow.ScrollRect.horizontalNormalizedPosition = 0; - } } diff --git a/Shared/Api/Helpers/FileIOHelper.cs b/Shared/Api/Helpers/FileIOHelper.cs index 6e55a7b87..6eaf2f449 100644 --- a/Shared/Api/Helpers/FileIOHelper.cs +++ b/Shared/Api/Helpers/FileIOHelper.cs @@ -1,4 +1,5 @@ using System.IO; +using MelonLoader.TinyJSON; using Newtonsoft.Json; using UnityEngine; namespace BTD_Mod_Helper.Api.Helpers; @@ -28,10 +29,12 @@ public static class FileIOHelper public static void SaveObject(string fileName, Il2CppSystem.Object data) { // TODO this needs il2cpp json - var text = JsonConvert.SerializeObject(data, new JsonSerializerSettings + /*var text = JsonConvert.SerializeObject(data, new JsonSerializerSettings { Formatting = Formatting.Indented - }); + });*/ + + var text = JSON.Dump(data, EncodeOptions.PrettyPrint); SaveFile(fileName, text); } diff --git a/Shared/ModHelper.cs b/Shared/ModHelper.cs index 3d18d4f43..859346ae0 100644 --- a/Shared/ModHelper.cs +++ b/Shared/ModHelper.cs @@ -17,7 +17,7 @@ public static class ModHelper #region ModHelperData for the Mod Helper internal const string Name = "BloonsTD6 Mod Helper"; - internal const string Version = "3.0.8"; + internal const string Version = "3.1.0-a1"; internal const string RepoOwner = "gurrenm3"; internal const string RepoName = "BTD-Mod-Helper"; internal const string Description = "A powerful and easy to use API for modding BTD6. Also the mod that is allowing all of this UI to happen right now :P";