diff --git a/ModMenu/Helpers.cs b/ModMenu/Helpers.cs index 5b92f92..a268939 100644 --- a/ModMenu/Helpers.cs +++ b/ModMenu/Helpers.cs @@ -2,6 +2,7 @@ using Kingmaker; using Kingmaker.Localization; using Kingmaker.Localization.Shared; +using Kingmaker.Utility; using Kingmaker.UI.MVVM._PCView.Settings.Entities; using Kingmaker.UI.MVVM._PCView.Settings; using Kingmaker.UI.MVVM._VM.Settings.Entities; @@ -19,12 +20,14 @@ namespace ModMenu internal static class Helpers { private static readonly List Strings = new(); + internal static LocalizedString EmptyString = CreateString("", ""); - internal static LocalizedString CreateString(string key, string enGB, string ruRU = "") + internal static LocalizedString CreateString(string key, string enGB, string ruRU = null, string zhCN = null, string deDE = null, string frFR = null) { - var localString = new LocalString(key, enGB, ruRU); + var localString = new LocalString(key, enGB, ruRU, zhCN, deDE, frFR); Strings.Add(localString); - localString.Register(); + if (LocalizationManager.Initialized) + localString.Register(); return localString.LocalizedString; } @@ -45,24 +48,43 @@ private class LocalString public readonly LocalizedString LocalizedString; private readonly string enGB; private readonly string ruRU; + private readonly string zhCN; + private readonly string deDE; + private readonly string frFR; + const string NullString = ""; - public LocalString(string key, string enGB, string ruRU) + public LocalString(string key, string enGB, string ruRU, string zhCN, string deDE, string frFR) { LocalizedString = new LocalizedString() { m_Key = key }; this.enGB = enGB; this.ruRU = ruRU; + this.zhCN = zhCN; + this.deDE = deDE; + this.frFR = frFR; } public void Register() { - var localized = enGB; - switch (LocalizationManager.CurrentPack.Locale) + string localized; + if (LocalizationManager.CurrentPack.Locale == Locale.enGB) { - case Locale.ruRU: - if (!string.IsNullOrEmpty(ruRU)) - localized = ruRU; - break; + localized = enGB; + goto putString; } + + localized = (LocalizationManager.CurrentPack.Locale) switch + { + Locale.ruRU => ruRU, + Locale.zhCN => zhCN, + Locale.deDE => deDE, + Locale.frFR => frFR, + _ => "" + }; + + if (localized.IsNullOrEmpty() || localized == NullString) + localized = enGB; + + ;putString: LocalizationManager.CurrentPack.PutString(LocalizedString.m_Key, localized); } } diff --git a/ModMenu/Main.cs b/ModMenu/Main.cs index 665ae0f..43f988a 100644 --- a/ModMenu/Main.cs +++ b/ModMenu/Main.cs @@ -1,27 +1,33 @@ using HarmonyLib; +using Kingmaker.Blueprints.Classes.Spells; using Kingmaker.Blueprints.JsonSystem; +using Kingmaker.Utility; using ModMenu.Settings; using System; +using System.Linq; +using System.Reflection; using static UnityModManagerNet.UnityModManager; using static UnityModManagerNet.UnityModManager.ModEntry; namespace ModMenu { - public static class Main + internal static class Main { internal static ModLogger Logger; - private static Harmony Harmony; + internal static ModEntry Entry; + internal static Harmony Harmony; public static bool Load(ModEntry modEntry) { try { + Entry = modEntry; Logger = modEntry.Logger; modEntry.OnUnload = OnUnload; Harmony = new(modEntry.Info.Id); + Harmony.DEBUG = true; Harmony.PatchAll(); - Logger.Log("Finished loading."); } catch (Exception e) @@ -56,6 +62,7 @@ static void Postfix() Logger.LogException("BlueprintsCache.Init", e); } } + } #endif } diff --git a/ModMenu/ModMenu.cs b/ModMenu/ModMenu.cs index fa92f2b..0204081 100644 --- a/ModMenu/ModMenu.cs +++ b/ModMenu/ModMenu.cs @@ -1,4 +1,5 @@ -using Kingmaker.Settings; +using JetBrains.Annotations; +using Kingmaker.Settings; using Kingmaker.UI.SettingsUI; using ModMenu.Settings; using System; @@ -25,8 +26,8 @@ public static class ModMenu /// public static void AddSettings(SettingsBuilder settings) { - var settingsGroup = settings.Build(); - foreach (var setting in settingsGroup.settings) + var result = settings.Build(); + foreach (var setting in result.settings) { if (Settings.ContainsKey(setting.Key)) { @@ -35,17 +36,19 @@ public static void AddSettings(SettingsBuilder settings) } Settings.Add(setting.Key, setting.Value); } - ModsMenuEntity.Add(settingsGroup.group); + ModsMenuEntity.Add(result.info, result.groups); } /// - /// Adds a new group of settings to the Mods menu page. + /// Adds a new entry containing the group of settings to the Mods menu dropdown. + /// Group's title will be displayed instead of the mod name. /// /// /// /// /// Using is recommended. If you prefer to construct the settings - /// on your own you can use this method. + /// on your own you can use this method, but better choose a less deprecated overload using ModsMenuEntry. + /// The name of the settings group will be used as the display name for the mod in the selection dropdown . /// /// /// @@ -53,10 +56,91 @@ public static void AddSettings(SettingsBuilder settings) /// . /// /// - public static void AddSettings(UISettingsGroup settingsGroup) - { - ModsMenuEntity.Add(settingsGroup); - } + /// + /// settingGroups argument must not be null, have at least 1 group in it and none can be null + /// + [Obsolete("Please, use AddSettings(ModsMenuEntry modInfo) instead")] + public static void AddSettings([NotNull] UISettingsGroup settingsGroup) + => ModsMenuEntity.Add(default, new UISettingsGroup[1] { settingsGroup }); + + /// + /// Adds a new entry containing groups of settings to the Mods menu dropdown. + /// The title of the first group will displayed instead of the mod name + /// + /// + /// + /// + /// Using is recommended. If you prefer to construct the settings + /// on your own you can use this method, but better choose a less deprecated overload using ModsMenuEntry. + /// The name of the first settings group from the list will be used as the display name for the mod in the selection dropdown . + /// + /// + /// + /// Settings added in this way cannot be retrieved using or + /// . + /// + /// + /// + /// settingGroups argument must not be null, have at least 1 group in it and none can be null + /// + [Obsolete("Please, use AddSettings(ModsMenuEntry modInfo) instead")] + public static void AddSettings([NotNull] List settingsGroups) + => ModsMenuEntity.Add(default, settingsGroups); + + /// + /// Adds a new entry containing groups of settings to the Mods menu page. + /// + /// + /// + /// + /// Use this method if you prefer to construct settings on your own; you can use this method + /// instead of using the recommended . + /// + /// + /// Structure containing basic info about the mod - name, version, description, author name et cetera. + /// This information will be displayed when user is choosing the mod from the dropdown + /// + /// Group of settings created with original Owlcat API. If you did not set any mod name inside the modInfo structure, + /// name of this settings group will be instead used as the display name of the mod. + /// + /// + /// + /// Settings added in this way cannot be retrieved using or + /// . + /// + /// + /// + /// settingGroups argument must not be null, have at least 1 group in it and none can be null + /// + /// + public static void AddSettings(Info modInfo, [NotNull] UISettingsGroup group) + => ModsMenuEntity.Add(modInfo, new UISettingsGroup[1] { group }); + + /// + /// Adds a new entry containing groups of settings to the Mods menu page. + /// + /// + /// + /// + /// Use this method if you prefer to construct settings on your own; you can use this method + /// instead of using the recommended . + /// + /// Settings added in this way cannot be retrieved using or + /// . + /// + /// + /// + /// Structure containing basic info about the mod - name, version, description, author name et cetera. + /// This information will be displayed when user is choosing the mod from the dropdown + /// param> + /// Groups of settings created with original Owlcat API. If you did not set any mod name inside the modInfo structure, + /// name of the first settings group from the list will be instead used as the display name of the mod. + /// param> + /// + /// settingGroups argument must not be null, have at least 1 group in it and none can be null + /// + public static void AddSettings(Info modInfo, [NotNull] List groups) + => ModsMenuEntity.Add(modInfo, groups); /// /// The setting with the specified , or null if it does not exist or has the wrong type. diff --git a/ModMenu/ModMenu.csproj b/ModMenu/ModMenu.csproj index 81e97db..d2ec005 100644 --- a/ModMenu/ModMenu.csproj +++ b/ModMenu/ModMenu.csproj @@ -45,22 +45,25 @@ - $(SolutionDir)lib\Assembly-CSharp.dll + $(SolutionDir)lib\Assembly-CSharp_public.dll + + ..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Pathfinder Second Adventure\Wrath_Data\Managed\Newtonsoft.Json.dll + + + $(SolutionDir)lib\Owlcat.Runtime.UI_public.dll + + + $(SolutionDir)lib\Unity.TextMeshPro_public.dll + $(WrathPath)\Wrath_Data\Managed\Assembly-CSharp-firstpass.dll - - $(WrathPath)\Wrath_Data\Managed\Owlcat.Runtime.UI.dll - $(WrathPath)\Wrath_Data\Managed\Owlcat.Runtime.Validation.dll - - $(WrathPath)\Wrath_Data\Managed\Unity.TextMeshPro.dll - $(WrathPath)\Wrath_Data\Managed\UnityEngine.CoreModule.dll @@ -89,16 +92,16 @@ - - - - - - + + + + + + + - - + diff --git a/ModMenu/NewTypes/DropdownItemWithHighlightCallback.cs b/ModMenu/NewTypes/DropdownItemWithHighlightCallback.cs new file mode 100644 index 0000000..8e36222 --- /dev/null +++ b/ModMenu/NewTypes/DropdownItemWithHighlightCallback.cs @@ -0,0 +1,56 @@ +using Owlcat.Runtime.UI.MVVM; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TMPro; +using UniRx.Triggers; +using UniRx; +using HarmonyLib; +using Epic.OnlineServices; +using UnityEngine.EventSystems; +using Steamworks; + +namespace ModMenu.NewTypes +{ + [HarmonyPatch] + internal class DropdownOptionWithHighlightCallback : TMP_Dropdown.OptionData + { + public List> OnMouseEnter = new(); + public List> OnMouseExit = new(); + + + [HarmonyPatch(typeof(TMP_Dropdown), nameof(TMP_Dropdown.AddItem))] + [HarmonyPostfix] + static void TMP_Dropdown_AddItem_Patch(TMP_Dropdown.OptionData data, TMP_Dropdown __instance, TMP_Dropdown.DropdownItem __result) + { + if (data is not DropdownOptionWithHighlightCallback workaround) + return; + else + { + var observer = __result.gameObject.AddComponent(); + foreach (var action in workaround.OnMouseEnter) + observer.PointerEnter += action; + foreach (var action in workaround.OnMouseExit) + observer.PointerExit += action; + } + } + + + class PointerObserverWorkaround : UIBehaviour, IPointerEnterHandler, IPointerExitHandler + { + internal event Action PointerEnter; + internal event Action PointerExit; + + public void OnPointerEnter(PointerEventData eventData) + { + PointerEnter?.Invoke(GetComponent()); + } + public void OnPointerExit(PointerEventData eventData) + { + PointerExit?.Invoke(GetComponent()); + } + } + } +} diff --git a/ModMenu/NewTypes/SettingsEntityCollapsibleHeader.cs b/ModMenu/NewTypes/SettingsEntityCollapsibleHeader.cs index 09df7b9..f72b4d5 100644 --- a/ModMenu/NewTypes/SettingsEntityCollapsibleHeader.cs +++ b/ModMenu/NewTypes/SettingsEntityCollapsibleHeader.cs @@ -100,7 +100,7 @@ public override VirtualListLayoutElementSettings LayoutSettings public OwlcatMultiButton Button; public ExpandableCollapseMultiButtonPC ButtonPC; - protected override void BindViewImplementation() + public override void BindViewImplementation() { Title.text = UIUtility.GetSaberBookFormat(ViewModel.Tittle, size: GetFontSize()); Button.OnLeftClick.RemoveAllListeners(); @@ -108,8 +108,8 @@ protected override void BindViewImplementation() ViewModel.Init(ButtonPC); } - protected virtual int GetFontSize() { return 140; } + public virtual int GetFontSize() { return 140; } - protected override void DestroyViewImplementation() { } + public override void DestroyViewImplementation() { } } } diff --git a/ModMenu/NewTypes/SettingsEntityDropdownButton.cs b/ModMenu/NewTypes/SettingsEntityDropdownButton.cs index 3792378..082acc6 100644 --- a/ModMenu/NewTypes/SettingsEntityDropdownButton.cs +++ b/ModMenu/NewTypes/SettingsEntityDropdownButton.cs @@ -7,8 +7,8 @@ using Owlcat.Runtime.UI.VirtualListSystem.ElementSettings; using System; using TMPro; -using UnityEngine.EventSystems; using UnityEngine; +using UnityEngine.EventSystems; using UnityEngine.UI; namespace ModMenu.NewTypes diff --git a/ModMenu/NewTypes/SettingsEntityImage.cs b/ModMenu/NewTypes/SettingsEntityImage.cs index bd2cbdd..da07839 100644 --- a/ModMenu/NewTypes/SettingsEntityImage.cs +++ b/ModMenu/NewTypes/SettingsEntityImage.cs @@ -37,7 +37,7 @@ internal SettingsEntityImageVM(UISettingsEntityImage imageEntity) ImageScale = imageEntity.ImageScale; } - protected override void DisposeImplementation() { } + public override void DisposeImplementation() { } } internal class SettingsEntityImageView : VirtualListElementViewBase @@ -81,7 +81,7 @@ private void SetHeight(float height) public Image Icon; public GameObject TopBorder; - protected override void BindViewImplementation() + public override void BindViewImplementation() { Icon.sprite = ViewModel.Sprite; @@ -114,7 +114,7 @@ protected override void BindViewImplementation() SetHeight(height); } - protected override void DestroyViewImplementation() { } + public override void DestroyViewImplementation() { } } } diff --git a/ModMenu/NewTypes/SettingsEntityModMenuEntry.cs b/ModMenu/NewTypes/SettingsEntityModMenuEntry.cs new file mode 100644 index 0000000..09cb1c1 --- /dev/null +++ b/ModMenu/NewTypes/SettingsEntityModMenuEntry.cs @@ -0,0 +1,11 @@ +using Kingmaker.Settings; +using ModMenu.Settings; + +namespace ModMenu.NewTypes +{ + internal class SettingsEntityModMenuEntry : SettingsEntity + { + internal static SettingsEntityModMenuEntry instance = new("modsmenu.entrystaticinstance", ModsMenuEntry.EmptyInstance); + private SettingsEntityModMenuEntry(string key, ModsMenuEntry defaultValue) : base(key, defaultValue, false, false, false) {} + } +} diff --git a/ModMenu/NewTypes/SettingsEntityPatches.cs b/ModMenu/NewTypes/SettingsEntityPatches.cs index 456c51f..ec12b3f 100644 --- a/ModMenu/NewTypes/SettingsEntityPatches.cs +++ b/ModMenu/NewTypes/SettingsEntityPatches.cs @@ -1,4 +1,11 @@ using HarmonyLib; +using Kingmaker; +using Kingmaker.Blueprints.Root; +using Kingmaker.Blueprints.Root.Strings; +using Kingmaker.Localization; +using Kingmaker.Settings; +using Kingmaker.UI.Common; +using Kingmaker.UI.MVVM; using Kingmaker.PubSubSystem; using Kingmaker.UI.MVVM._PCView.ServiceWindows.Journal; using Kingmaker.UI.MVVM._PCView.Settings; @@ -16,10 +23,16 @@ using Owlcat.Runtime.UI.VirtualListSystem; using Owlcat.Runtime.UI.VirtualListSystem.ElementSettings; using System; +using System.CodeDom; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; using System.Reflection; +using System.Reflection.Emit; using TMPro; using UnityEngine; using UnityEngine.UI; +using static Kingmaker.UI.SettingsUI.UISettingsManager; using Object = UnityEngine.Object; namespace ModMenu.NewTypes @@ -29,6 +42,70 @@ internal class SettingsEntityPatches internal static readonly FieldInfo OverrideType = AccessTools.Field(typeof(VirtualListLayoutElementSettings), "m_OverrideType"); + /// + /// Patch to prevent exceptions on deserializing settings + /// + [HarmonyPatch] + static class DictionarySettingsProviderPatcher + { + [HarmonyTargetMethod] + static MethodInfo TargetMethod() + { + return typeof(DictionarySettingsProvider) + .GetMethod(nameof(DictionarySettingsProvider.GetValue)) + .MakeGenericMethod(typeof(ModsMenuEntry)); + } + + [HarmonyPrefix] + public static bool DeserializeSettingEntry(string key, ref ModsMenuEntry __result) + { + if (key.Equals(SettingsEntityModMenuEntry.instance.Key)) + { + __result = ModsMenuEntry.EmptyInstance; + return false; + } + return true; + } + } + + /// + /// Patch to change the way dropdown options are generated so that the settings description would show individual mod descriptions. + /// + [HarmonyPatch] + static class SettingsEntityDropdownPCView_Patch + { + [HarmonyPatch(typeof(SettingsEntityDropdownPCView), nameof(SettingsEntityDropdownPCView.SetupDropdown))] + static bool Prefix(SettingsEntityDropdownPCView __instance) + { + if (__instance.ViewModel.m_UISettingsEntity is not UISettingsEntityDropdownModMenuEntry) return true; + + else + __instance.Dropdown.gameObject.SetActive(true); + __instance.Dropdown.ClearOptions(); + + List options = new(); + var vm = GameObject.Find("CommonPCView(Clone)/Canvas/SettingsView/")?.GetComponent()?.ViewModel; + foreach (var modEntry in ModsMenuEntity.ModEntries) + { + options.Add( + new DropdownOptionWithHighlightCallback() + { + m_Text = modEntry.ModInfo.ModName, + OnMouseEnter = new() { new(_ => { + if (vm is null) + Main.Logger.Warning("SettingsEntityDropdownPCView_Patch - settings VM is null!"); + else + vm.HandleShowSettingsDescription( + title: UIUtility.GetSaberBookFormat(modEntry.ModInfo.ModName, default(Color), 140, null, 0f), + description: modEntry.ModInfo.GenerateDescription()); + })} + }); + } + __instance.Dropdown.AddOptions(options); + return false; + } + } + /// /// Patch to return the correct view model for /// @@ -73,7 +150,49 @@ static bool Prefix( return true; } - [HarmonyPatch(nameof(SettingsVM.SwitchSettingsScreen)), HarmonyPostfix] + [HarmonyPatch(nameof(SettingsVM.SwitchSettingsScreen)), HarmonyPrefix] + static bool Prefix(UISettingsManager.SettingsScreen settingsScreen, SettingsVM __instance) + { + if (settingsScreen != ModsMenuEntity.SettingsScreenId) return true; + try + { + Main.Logger.NativeLog("Collecting setting entities."); + + __instance.m_SettingEntities.Clear(); + __instance.m_SettingEntities.Add(__instance.AddDisposableAndReturn(SettingsVM.GetVMForSettingsItem(UISettingsEntityDropdownModMenuEntry.instance))); + if (UISettingsEntityDropdownModMenuEntry.instance.Setting.GetTempValue() == ModsMenuEntry.EmptyInstance) + return false; + //__instance.m_SettingEntities.Add(__instance.AddDisposableAndReturn(SettingsVM.GetVMForSettingsItem(separator))); + + //Here should be a toggle for mod disabling, but do we need it? + SettingsEntitySubHeaderVM subheader; + foreach (var uisettingsGroup in ModsMenuEntity.CollectSettingGroups) + { + __instance.m_SettingEntities.Add(__instance.AddDisposableAndReturn(new SettingsEntityHeaderVM(uisettingsGroup.Title))); + subheader = null; + foreach (UISettingsEntityBase uisettingsEntityBase in uisettingsGroup.VisibleSettingsList) + { + if (uisettingsEntityBase is UISettingsEntitySubHeader sub) + { + subheader = new SettingsEntitySubHeaderVM(sub); + __instance.m_SettingEntities.Add(__instance.AddDisposableAndReturn(subheader)); + continue; + } + VirtualListElementVMBase element = __instance.AddDisposableAndReturn(SettingsVM.GetVMForSettingsItem(uisettingsEntityBase)); + __instance.m_SettingEntities.Add(element); + subheader?.SettingsInGroup.Add(element); + } + } + + } + catch (Exception e) + { + Main.Logger.LogException("SettingsVM.SwitchSettingsScreen", e); + } + return false; + } + + /*[HarmonyPatch(nameof(SettingsVM.SwitchSettingsScreen)), HarmonyPostfix] static void Postfix(UISettingsManager.SettingsScreen settingsScreen, SettingsVM __instance) { try @@ -112,7 +231,7 @@ static void Postfix(UISettingsManager.SettingsScreen settingsScreen, SettingsVM { Main.Logger.LogException("SettingsVM.SwitchSettingsScreen", e); } - } + }*/ } /// @@ -331,6 +450,187 @@ private static SettingsEntityDropdownButtonView CreateDropdownButtonTemplate( return templatePrefab; } + } + + [HarmonyPatch] + internal static class DefaultButtonPatcher + { + [HarmonyPatch(typeof(SettingsVM), nameof(SettingsVM.SetSettingsList))] + [HarmonyTranspiler] + static IEnumerable SettingsVM_SetSettingsList_Transpiler_ToEnableDefaultButtonOnModsTab(IEnumerable instructions) + { + var _inst = instructions.ToList(); + int length = _inst.Count; + int index = -1; + for (int i = 0; i < length; i++) + { + if ( + _inst[i + 0].opcode == OpCodes.Ldloc_0 && + _inst[i + 1].opcode == OpCodes.Ldfld && _inst[i + 1].operand is FieldInfo fi && fi.Name.Contains("settingsScreen") && + _inst[i + 2].opcode == OpCodes.Ldc_I4_4 && + _inst[i + 3].opcode == OpCodes.Beq_S || _inst[i + 3].opcode == OpCodes.Beq) + { + index = i; + break; + } + } + + if (index == -1) + { + Main.Logger.Error("DefaultButtonPatcher - failed to find the index when transpile SettingsVM.SetSettingsList. Default button will not be enabled on the Mods tab of settings screen."); + return instructions; + } + + _inst.InsertRange(index + 4, new CodeInstruction[4] { + new (_inst[index + 0]), + new (_inst[index + 1]), + new (OpCodes.Ldc_I4, ModsMenuEntity.SettingsScreenValue), + new (_inst[index + 3]), + }); + + return _inst; + } + + /// + /// Will make Default button affect the mod selected on the Mod tab + /// + /// + [HarmonyPatch(typeof(SettingsController), nameof(SettingsController.ResetToDefault))] + [HarmonyTranspiler] + static IEnumerable SettingsController_ResetToDefault_Transpiler_ToCollectModSettings(IEnumerable instructions, ILGenerator gen) + { + var _inst = instructions.ToList(); + int length = _inst.Count; + int index = -1; + FieldInfo settingsManagerInfo = typeof(Kingmaker.Game).GetField(nameof(Kingmaker.Game.UISettingsManager)); + MethodInfo gameGetter = typeof(Kingmaker.Game).GetProperty(nameof(Kingmaker.Game.Instance)).GetMethod; + + + for (int i = 0; i < length; i++) + { + if ( + ((_inst[i + 0].opcode == OpCodes.Call || _inst[i + 0].opcode == OpCodes.Callvirt) && _inst[i + 0].operand is MethodInfo mi1 && mi1 == gameGetter) && + (_inst[i + 1].opcode == OpCodes.Ldfld && _inst[i + 1].operand is FieldInfo fi && fi == settingsManagerInfo) && + _inst[i + 2].opcode == OpCodes.Ldarg_0 && + _inst[i + 3].opcode == OpCodes.Newobj && + ((_inst[i + 4].opcode == OpCodes.Call || _inst[i + 4].opcode == OpCodes.Callvirt) && _inst[i + 4].operand is MethodInfo mi2 && mi2.Name.Contains("GetSettingsList"))) + { + index = i; + break; + } + } + + if (index == -1) + { + Main.Logger.Error("DefaultButtonPatcher - failed to find the index when transpile SettingsController.ResetToDefault. Default button will do nothing on the Mods tab."); + return instructions; + } + + Label labelNotMods = gen.DefineLabel(); + _inst[index].labels.Add(labelNotMods); + + Label labelIsMods = gen.DefineLabel(); + _inst[index+5].labels.Add(labelIsMods); + + MethodInfo mi = typeof(Enumerable).GetMethod(nameof(Enumerable.ToList)).MakeGenericMethod(typeof(UISettingsGroup)); + + _inst.InsertRange(index, new CodeInstruction[] { + new CodeInstruction(OpCodes.Ldarg_0), + //CodeInstruction.Call((UISettingsManager.SettingsScreen e) => Convert.ToInt32(e)), //WHY DOES IT NOT WORK?!?!?!?!?! + //new CodeInstruction(OpCodes.Ldc_I4, ModsMenuEntity.SettingsScreenValue), + //new CodeInstruction(OpCodes.Ceq), + CodeInstruction.Call((UISettingsManager.SettingsScreen e) => AnotherScreenCheck(e)), + new CodeInstruction(OpCodes.Brfalse_S, labelNotMods), + new CodeInstruction(OpCodes.Call, typeof(ModsMenuEntity).GetProperty(nameof(ModsMenuEntity.CollectSettingGroups), BindingFlags.Static | BindingFlags.NonPublic).GetMethod), + new CodeInstruction(OpCodes.Callvirt, mi), + new CodeInstruction(OpCodes.Br_S, labelIsMods) + });; + + return _inst; + } + + static bool AnotherScreenCheck(UISettingsManager.SettingsScreen e) => e == (UISettingsManager.SettingsScreen)ModsMenuEntity.SettingsScreenValue; + + [HarmonyPatch(typeof(SettingsVM), nameof(SettingsVM.OpenDefaultSettingsDialog))] + [HarmonyTranspiler] + static internal IEnumerable SettingsVM_OpenDefaultSettingsDialog_Transpiler_ToChangeDefaultDialogMessage(IEnumerable instructions, ILGenerator gen) + { + var _inst = instructions.ToList(); + int length = _inst.Count; + int indexStart = -1; + int indexEnd = -1; + newDefaultMessage = Helpers.CreateString( + key: "ModsMenuNewDefaultButtonMessage", + enGB: "Revert all settings of the mod {0} to their default values?", + ruRU: "Вернуть все настройки для мода {0} к их значениям по-умолчанию?", + zhCN: "还原所有{0}模组设置到默认值?", + deDE: "Alle Einstellungen des Mods {0} auf ihre Standardwerte zurücksetzen?", + frFR: "Rétablir les valeurs par défaut de tous les paramètres du mod {0}?"); + + for (int i = 0; i < length; i++) + { + if ( + _inst[i].Calls(typeof(Kingmaker.Game).GetProperty(nameof(Kingmaker.Game.Instance)).GetMethod) && + _inst[i + 1].Calls(typeof(Kingmaker.Game).GetProperty(nameof(Kingmaker.Game.BlueprintRoot)).GetMethod) && + _inst[i + 2].opcode == OpCodes.Ldfld && _inst[i + 2].operand is FieldInfo fi1 && fi1 == AccessTools.Field(typeof(BlueprintRoot), nameof(BlueprintRoot.LocalizedTexts)) && + _inst[i + 3].opcode == OpCodes.Ldfld && _inst[i + 3].operand is FieldInfo fi2 && fi2 == AccessTools.Field(typeof(LocalizedTexts), nameof(LocalizedTexts.UserInterfacesText)) && + _inst[i + 4].opcode == OpCodes.Ldfld && _inst[i + 4].operand is FieldInfo fi3 && fi3 == AccessTools.Field(typeof(UIStrings), nameof(UIStrings.SettingsUI)) && + _inst[i + 5].opcode == OpCodes.Ldfld && _inst[i + 5].operand is FieldInfo fi4 && fi4 == AccessTools.Field(typeof(UITextSettingsUI), nameof(UITextSettingsUI.RestoreAllDefaultsMessage)) + ) + { + indexStart = i; + break; + } + } + + if (indexStart == -1) + { + Main.Logger.Error("DefaultButtonPatcher - failed to find the starting index when transpile SettingsVM.OpenDefaultSettingsDialog. Default button message will not be altered."); + return instructions; + } + + for (int i = indexStart + 6; i < length; i++) + { + if ( + _inst[i].opcode == OpCodes.Call && _inst[i].operand is MethodInfo { Name: nameof(string.Format)} && + _inst[i + 1].opcode == OpCodes.Stfld && _inst[i + 1].operand is FieldInfo { Name: "text" } + ) + { + indexEnd = i; + break; + } + } + + if (indexEnd == -1) + { + Main.Logger.Error("DefaultButtonPatcher - failed to find the ending index when transpile SettingsVM.OpenDefaultSettingsDialog. Default button message will not be altered."); + return instructions; + } + + Label labelNotMod = gen.DefineLabel(); + _inst[indexStart].labels.Add(labelNotMod); + + Label labelIsMod = gen.DefineLabel(); + _inst[indexEnd +1].labels.Add(labelIsMod); + + _inst.InsertRange(indexStart, new CodeInstruction[] + { + CodeInstruction.Call(() => CheckForSelectedSettingsScreenType()), + new CodeInstruction(OpCodes.Brfalse_S, labelNotMod), + CodeInstruction.Call(() => MakeMeDefaultButtonMessage()), + new CodeInstruction(OpCodes.Br_S, labelIsMod) + }); + + return _inst; + } + static LocalizedString newDefaultMessage; + static bool CheckForSelectedSettingsScreenType() => RootUIContext.Instance?.CommonVM.SettingsVM.Value?.SelectedMenuEntity.Value?.SettingsScreenType == (UISettingsManager.SettingsScreen)ModsMenuEntity.SettingsScreenValue; + + static string MakeMeDefaultButtonMessage() + { + return string.Format(newDefaultMessage, SettingsEntityModMenuEntry.instance.m_TempValue.ModInfo.ModName); + } + [HarmonyPatch(typeof(SettingsVM))] static class ApplySettings_Patch { diff --git a/ModMenu/NewTypes/SettingsEntitySubHeader.cs b/ModMenu/NewTypes/SettingsEntitySubHeader.cs index aca4943..a199ae1 100644 --- a/ModMenu/NewTypes/SettingsEntitySubHeader.cs +++ b/ModMenu/NewTypes/SettingsEntitySubHeader.cs @@ -51,7 +51,7 @@ public override VirtualListLayoutElementSettings LayoutSettings } private VirtualListLayoutElementSettings m_LayoutSettings; - protected override int GetFontSize() + public override int GetFontSize() { return 110; } diff --git a/ModMenu/NewTypes/UISettingsEntityDropDownModMenuEntry.cs b/ModMenu/NewTypes/UISettingsEntityDropDownModMenuEntry.cs new file mode 100644 index 0000000..5786a1f --- /dev/null +++ b/ModMenu/NewTypes/UISettingsEntityDropDownModMenuEntry.cs @@ -0,0 +1,58 @@ +using HarmonyLib; +using Kingmaker.Settings; +using Kingmaker.UI.SettingsUI; +using ModMenu.Settings; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ModMenu.NewTypes +{ + [HarmonyPatch] + internal class UISettingsEntityDropdownModMenuEntry : UISettingsEntityDropdown + { + static UISettingsEntityDropdownModMenuEntry() + { + instance = CreateInstance(); + instance.m_Description = Helpers.CreateString("UISettingsEntityDropdownModMenuEntry.Description", + enGB:"Select a mod", + ruRU: "Выберите мод", + zhCN: "选择一个模组", + deDE: "Wähle einen Mod aus", + frFR: "Choisir un mod"); + instance.m_TooltipDescription = Helpers.EmptyString; + + instance.LinkSetting(SettingsEntityModMenuEntry.instance); + + ((IUISettingsEntityDropdown) instance).OnTempIndexValueChanged += + new (ModIndex => ModsMenuEntity.settingVM.SwitchSettingsScreen(ModsMenuEntity.SettingsScreenId)); + + ((IUISettingsEntityDropdown) instance).OnTempIndexValueChanged += + new (_ => + { + SettingsController.RemoveFromConfirmationList(instance.SettingsEntity, false); + SettingsEntityModMenuEntry.instance.TempValueIsConfirmed = true; + }); + + } + + internal static UISettingsEntityDropdownModMenuEntry instance; + + public override List LocalizedValues + => ModsMenuEntity.ModEntries.Select(entry => entry.ModInfo.ModName.ToString()).ToList(); + public override int GetIndexTempValue() + => ModsMenuEntity.ModEntries.IndexOf(Setting.GetTempValue()); + + + public override void SetIndexTempValue(int value) + { + if (value is < 0 && value > ModsMenuEntity.ModEntries.Count()) + { + Main.Logger.Error($"Value {value} is given to UISettingsEntityDropdownModMenuEntry when there're only {ModsMenuEntity.ModEntries.Count()} entries in the list"); + SetTempValue(ModsMenuEntity.ModEntries[0]); + } + + SetTempValue(ModsMenuEntity.ModEntries[value]); + } + } +} diff --git a/ModMenu/Settings/KeyBinding.cs b/ModMenu/Settings/KeyBinding.cs index 9ca04c9..f6f3e33 100644 --- a/ModMenu/Settings/KeyBinding.cs +++ b/ModMenu/Settings/KeyBinding.cs @@ -1,16 +1,14 @@ -using Kingmaker.Localization; +using HarmonyLib; +using Kingmaker; +using Kingmaker.Localization; using Kingmaker.Settings; -using Kingmaker.UI.SettingsUI; -using System.Text; -using static Kingmaker.UI.KeyboardAccess; -using UnityEngine; -using HarmonyLib; using Kingmaker.UI; -using Kingmaker; +using Kingmaker.UI.SettingsUI; using System; using System.Collections.Generic; -using System.Security.Principal; -using Kingmaker.GameModes; +using System.Text; +using UnityEngine; +using static Kingmaker.UI.KeyboardAccess; namespace ModMenu.Settings { diff --git a/ModMenu/Settings/ModsMenuEntity.cs b/ModMenu/Settings/ModsMenuEntity.cs index 7cd78e8..303b822 100644 --- a/ModMenu/Settings/ModsMenuEntity.cs +++ b/ModMenu/Settings/ModsMenuEntity.cs @@ -1,11 +1,14 @@ using HarmonyLib; +using JetBrains.Annotations; using Kingmaker.Localization; using Kingmaker.UI.MVVM._PCView.Settings.Menu; using Kingmaker.UI.MVVM._VM.Settings; using Kingmaker.UI.SettingsUI; using Kingmaker.Utility; +using ModMenu.NewTypes; using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Reflection.Emit; @@ -17,27 +20,35 @@ namespace ModMenu.Settings internal class ModsMenuEntity { // Random magic number representing our fake enum for UiSettingsManager.SettingsScreen - private const int SettingsScreenValue = 17; + internal const int SettingsScreenValue = 17; internal static readonly UISettingsManager.SettingsScreen SettingsScreenId = (UISettingsManager.SettingsScreen)SettingsScreenValue; + internal static SettingsVM settingVM; + private static LocalizedString _menuTitleString; private static LocalizedString MenuTitleString { get { _menuTitleString ??= Helpers.CreateString( - "ModsMenu.Title", "Mods", ruRU: "Моды"); + "ModsMenu.Title", "Mods", ruRU: "Моды", zhCN: "模组", deDE: "Mods", frFR: "Mods"); return _menuTitleString; } } - private static readonly List ModSettings = new(); + internal static readonly List ModEntries = new(); - internal static void Add(UISettingsGroup uiSettingsGroup) - { - ModSettings.Add(uiSettingsGroup); - } + + internal static void Add(Info modInfo, [NotNull] IEnumerable settingGroups) + => ModEntries.Add(new(modInfo, settingGroups)); + + internal static void Add(ModsMenuEntry modEntry) + => ModEntries.Add(modEntry); + + + internal static IEnumerable CollectSettingGroups => + UISettingsEntityDropdownModMenuEntry.instance.Setting.m_TempValue.ModSettings; /// /// Patch to create the Mods Menu ViewModel. @@ -85,6 +96,7 @@ private static void AddMenuEntity(SettingsVM settings) try { settings.CreateMenuEntity(MenuTitleString, SettingsScreenId); + settingVM = settings; Main.Logger.NativeLog("Added Mods Menu ViewModel."); } catch (Exception e) @@ -94,6 +106,7 @@ private static void AddMenuEntity(SettingsVM settings) } } + /// /// Patch to create the Mods Menu View. Needed to show the menu in-game. /// @@ -137,7 +150,7 @@ static void Postfix(UISettingsManager.SettingsScreen? screenId, ref List + /// Wrapper class used to display mod's settings in the ModMenu dropdown. Contains a list of UI Setting Groups and + /// modification's info such as name. + /// + internal class ModsMenuEntry : IConvertible + { +#pragma warning disable CS1591 // stupid documentation requests + #region Conversion + public TypeCode GetTypeCode() + { + return TypeCode.Int32; + } + + public bool ToBoolean(IFormatProvider provider) + { + return false; + } + + public char ToChar(IFormatProvider provider) + { + return (char)0; + } + + public sbyte ToSByte(IFormatProvider provider) + { + return 0; + } + + public byte ToByte(IFormatProvider provider) + { + return 0; + } + + public short ToInt16(IFormatProvider provider) + { + return 0; + } + + public ushort ToUInt16(IFormatProvider provider) + { + return 0; + } + + public int ToInt32(IFormatProvider provider) + { + return 0; + } + + public uint ToUInt32(IFormatProvider provider) + { + return 0; + } + + public long ToInt64(IFormatProvider provider) + { + return 0; + } + + public ulong ToUInt64(IFormatProvider provider) + { + return 0; + } + + public float ToSingle(IFormatProvider provider) + { + return 0; + } + + public double ToDouble(IFormatProvider provider) + { + return 0; + } + + public decimal ToDecimal(IFormatProvider provider) + { + return 0; + } + + public DateTime ToDateTime(IFormatProvider provider) + { + throw new NotImplementedException(); + } + + public string ToString(IFormatProvider provider) + { + return "0"; + } + + public object ToType(Type conversionType, IFormatProvider provider) + { + if (conversionType == typeof(int)) + return 0; + throw new NotImplementedException(); + } + #endregion + static ModsMenuEntry() + { + Info info = new(Helpers.EmptyString, Helpers.EmptyString); + UISettingsGroup pseudoGroup = ScriptableObject.CreateInstance(); + pseudoGroup.Title = Helpers.EmptyString; + EmptyInstance = new(info, new UISettingsGroup[1] { pseudoGroup }); + ModsMenuEntity.Add(EmptyInstance); + } + + internal static ModsMenuEntry EmptyInstance; + + internal readonly IEnumerable ModSettings; + internal readonly Info ModInfo; + + /// + /// settingGroups argument must not be null, have at least 1 group in it and none can be null + /// + internal ModsMenuEntry(Info modInfo, [NotNull] IEnumerable settingGroups) + { + if (settingGroups is null || settingGroups.Count() == 0) + throw new ArgumentException("Cannot create ModsMenuEntry without any settingsGroups."); + + if (settingGroups.Any(g => g is null)) + throw new ArgumentException("Cannot create ModsMenuEntry with a null settingsGroup."); + + ModSettings = settingGroups; + if (!modInfo.Equals(default(Info))) + ModInfo = modInfo; + else + ModInfo = new(settingGroups.ElementAt(0)?.Title); + + } + + } + /// + /// Structure containing information required to display the ModEntry in the ModMenu dropdown (such as mod's info) + /// + public struct Info + { + private static readonly LocalizedString AnonymousMod = + Helpers.CreateString("ModsMenu.AnonymousMod", "Anonymous Mod", ruRU: "Безымянный мод", zhCN: "匿名模组", deDE: "Anonymer Mod", frFR: "Mod anonyme"); + private static readonly LocalizedString stringAuthor = + Helpers.CreateString("ModsMenu.stringAuthor", "Author", ruRU: "Создатель", zhCN: "作者", deDE: "Autor", frFR: "Créateur"); + private static readonly LocalizedString stringVer = + Helpers.CreateString("ModsMenu.stringVer", "Version", ruRU: "Версия", zhCN: "版本", deDE: "Version", frFR: "Version"); + private static int AnonymousCounter = 0; + + public Sprite ModImage { get; private set; } + public LocalizedString ModName { get; private set; } + public string VersionNumber { get; private set; } + public string AuthorName { get; private set; } + public LocalizedString LocalizedModDescription { get; private set; } + public string NonLocalizedModDescription { get; private set; } + private string ModDescription { get { return LocalizedModDescription ?? NonLocalizedModDescription; } } + private string m_CachedDescription; + private Locale m_LastLocale; + internal bool AllowModDisabling { get; set; } + internal OwlcatModification OwlMod { get; } + internal UnityModManager.ModEntry UMMMod { get; } + + /// + /// Name of your mod displayed in the ModMenu dropdown. If you don't provide any, it will be Anonymous + /// + /// + /// Description which will be displayed when user selects your mod in ModMenu dropdown + /// + /// + /// Mod version to be displayed alongside the description + /// + /// + /// Mod author's name to be displayed alongside the description + /// + /// + /// Mod's icon to be displayed alongside the description + /// + public Info( + LocalizedString name, + LocalizedString description = null, + string version = "", + string author = "", + Sprite image = null) + { + if (name is null) ModName = AnonymousMod; + else ModName = name; + VersionNumber = version; + AuthorName = author; + ModImage = image; + LocalizedModDescription = description; + } + + /// + /// Name of your mod which will be displayed in the ModMenu dropdown. If you don't provide any, it will displayed + /// as Anonymous + /// + /// + /// Description which will be displayed when user selects your mod in ModMenu dropdown + /// + /// Mod version to be displayed alongside the description + /// Mod author's name to be displayed alongside the description + /// Mod's icon to be displayed alongside the description + public Info( + string name, string description = "", string version = "", string author = "", Sprite image = null) : + this(Helpers.CreateString($"ModsMenu.{name}.Name", name), null, version, author, image) + { + if (name.IsNullOrEmpty()) + AnonymousCounter++; + + NonLocalizedModDescription = description; + } + + /// + /// If your mod is an OwlcatModification, you may provide a link to it in this case the constructor will use the + /// information from manifest to set mod's name, description, author and version. + /// + /// + /// Set to true if you want to have a button in the ModMenu to disable your mod + /// + /// + /// Localized name for your mod if you are not satisfied with the non localized name taken from the mod manifest + /// + /// + /// Localized description for your mod if you are mot satisfied with the non localized description taken from the + /// mod manifest + /// + /// Mod's icon to be displayed alongside the description + /// + /// You are not allowed to provide a null OwlcatModification when using this constructor + /// + public Info( + OwlcatModification owlcatModification, + bool allowModDisabling, + LocalizedString localizedModName = null, + LocalizedString localizedModDescription = null, + Sprite image = null) + { + if (owlcatModification is null) + throw new ArgumentException("Attempt to create ModInfo out of a null OwlcatModification"); + + OwlMod = owlcatModification; + AllowModDisabling = allowModDisabling; + OwlcatModificationManifest manifest = owlcatModification.Manifest; + AuthorName = manifest.Author; + VersionNumber = manifest.Version; + + string uniqueName = manifest.UniqueName; + if (uniqueName.IsNullOrEmpty()) + { + uniqueName = "AnonymousMod" + AnonymousCounter; + AnonymousCounter++; + } + + if (localizedModName is not null && localizedModName.ToString().IsNullOrEmpty() is false) + ModName = localizedModName; + else + { + ModName = manifest.DisplayName.IsNullOrEmpty() is false + ? Helpers.CreateString($"ModsMenu.{uniqueName}.ModName", manifest.DisplayName) + : AnonymousMod; + } + + if (localizedModDescription is not null && localizedModDescription.ToString().IsNullOrEmpty() is false) + LocalizedModDescription = localizedModDescription; + else if (manifest.Description.IsNullOrEmpty() is false) + NonLocalizedModDescription = Helpers.CreateString($"ModsMenu.{uniqueName}.Description", manifest.DisplayName); + + ModImage = image; + } + + /// if your mod is an UMM mod, you may provide a link to it + /// in this case the constructor will use the information from manifest to set mod's name, author and version. + /// Set to true if you want to have a button in the ModMenu to disable your mod + /// Localized name for your mod if you are mot satisfied with the non localized name taken from the mod manifest + /// Localized description for your mod + /// Mod's icon to be displayed alongside the description + /// You are not allowed to provide a null UnityModManager.ModEntry when using this constructor + public Info( + ModEntry ummMod, + bool allowModDisabling, + LocalizedString localizedModName = null, + LocalizedString localizedModDescription = null, + Sprite image = null) + { + if (ummMod is null) + throw new ArgumentException("Attempt to create ModInfo out of a null UMM mod"); + + UMMMod = ummMod; + AllowModDisabling = allowModDisabling; + UnityModManager.ModInfo info = ummMod.Info; + AuthorName = info.Author; + VersionNumber = info.Version; + + string uniqueName = info.Id; + if (uniqueName.IsNullOrEmpty()) + { + uniqueName = "AnonymousMod" + AnonymousCounter; + AnonymousCounter++; + } + AuthorName = info.Author; + + if (localizedModName is not null && localizedModName.ToString().IsNullOrEmpty() is false) + ModName = localizedModName; + else if (info.DisplayName.IsNullOrEmpty() is false) + ModName = Helpers.CreateString($"ModsMenu.{uniqueName}.ModName", info.DisplayName); + else + ModName = AnonymousMod; + + if (localizedModDescription is not null && localizedModDescription.ToString().IsNullOrEmpty() is false) + LocalizedModDescription = localizedModDescription; + + ModImage = image; + } + + internal string GenerateDescription() + { + if (!m_CachedDescription.IsNullOrEmpty() && m_LastLocale == LocalizationManager.CurrentLocale) + return m_CachedDescription; + + else + try + { + string result = ""; + if (!string.IsNullOrEmpty(AuthorName)) + result += $"{stringAuthor}: {AuthorName}\n"; + + if (!string.IsNullOrEmpty(VersionNumber)) + result += $"({stringVer}: {VersionNumber})\n"; + + //result = $"{result}\n"; + result += "\n"; + result += $"{ModDescription}"; + m_CachedDescription= result; + m_LastLocale = LocalizationManager.CurrentLocale; + return result; + } + catch(Exception ex) + { + Main.Logger.Log("We fucked up generating description!"); + Main.Logger.LogException(ex); + return "We fucked up generating description!"; + } + } + } +} diff --git a/ModMenu/Settings/SettingsBuilder.cs b/ModMenu/Settings/SettingsBuilder.cs index 4a7eade..de46938 100644 --- a/ModMenu/Settings/SettingsBuilder.cs +++ b/ModMenu/Settings/SettingsBuilder.cs @@ -1,5 +1,6 @@ using Kingmaker; using Kingmaker.Localization; +using Kingmaker.Modding; using Kingmaker.PubSubSystem; using Kingmaker.Settings; using Kingmaker.UI; @@ -9,9 +10,11 @@ using System.Collections.Generic; using System.Linq; using UnityEngine; +using UnityModManagerNet; namespace ModMenu.Settings { +#pragma warning disable CS1591 // stupid documentation requests /// /// Builder API for constructing settings. /// @@ -70,7 +73,15 @@ namespace ModMenu.Settings /// public class SettingsBuilder { - private readonly UISettingsGroup Group = ScriptableObject.CreateInstance(); + private object Mod; + private bool AllowDisabling; + private LocalizedString ModName; + private LocalizedString ModDescription; + private string Author = ""; + private string Version = ""; + private Sprite modIllustration; + private readonly List GroupList = new(); + private UISettingsGroup Group; // it will be now initialized by the instance constructor through a call to AddAnotherSettingsGroup. private readonly List Settings = new(); private readonly Dictionary SettingsEntities = new(); private Action OnDefaultsApplied; @@ -81,13 +92,95 @@ public class SettingsBuilder /// Title of the settings group, displayed on the settings page public static SettingsBuilder New(string key, LocalizedString title) { - return new(key, title); + return new SettingsBuilder(key, title); } - public SettingsBuilder(string key, LocalizedString title) + /// + /// Globally unique key / name for the settings group. Use only lowercase letters, numbers, '-', and '.' + /// + /// Title of the settings group, displayed on the settings page + public SettingsBuilder(string key, LocalizedString title) : this() + { + AddAnotherSettingsGroup(key, title); + } + + private SettingsBuilder() { } + + /// + /// Creates a new group of settings. + /// + /// + /// Globally unique key / name for the settings group. Use only lowercase letters, numbers, '-', and '.' + /// + /// Title of the settings group, displayed on the settings page + public SettingsBuilder AddAnotherSettingsGroup(string key, LocalizedString title) + { + if (GroupList.Any() && GroupList.Any(g => g.name.Equals(key))) + Main.Logger.Warning("An attempt to create a new Settings group with an existing key"); + + var group = ScriptableObject.CreateInstance(); + group.name = key; + group.Title= title; + if (Settings?.Count > 0) + { + Group.SettingsList = Settings.ToArray(); + Settings.RemoveRange(0, Settings.Count - 1); + } + GroupList.Add(group); + Group = group; + return this; + } + + /// + /// Set a source mod for your settings. Information from the manifest / info will be displayed when selecting the + /// mod in the menu dropdown. If you don't set Mod name and Mod description separately, the information from the + /// manifest / info will be used instead. Providing the source mod is necessary if you want to allow disabling from + /// the ModMenu. + /// + /// + /// Your mod + /// Set to true if you want to create a button to disable it + public SettingsBuilder SetMod(OwlcatModification modEntry, bool allowDisabling = false) + { + Mod = modEntry; + AllowDisabling = allowDisabling; + return this; + } + + /// + /// Set a source mod for your settings. Information from the manifest / info will be displayed when selecting the + /// mod in the menu dropdown. If you don't set Mod name and Mod description separately, the information from the + /// manifest / info will be used instead. Providing the source mod is necessary if you want to allow disabling from + /// the ModMenu. + /// + /// + /// Your mod + /// Set to true if you want to create a button to disable it + public SettingsBuilder SetMod(UnityModManager.ModEntry modEntry, bool allowDisabling = false) + { + Mod = modEntry; + AllowDisabling = allowDisabling; + return this; + } + + /// + /// The name of your mod to be displayed in the ModMenu dropdown. + /// + /// A Localized string containing the mod name. + public SettingsBuilder SetModName(LocalizedString name) { - Group.name = key.ToLower(); - Group.Title = title; + ModName = name; + return this; + } + + /// + /// Sets the description for your mod which will be visible when hovering the mouse over the mod entry in the ModMenu dropdown. + /// + /// A Localized string containing the description. + public SettingsBuilder SetModDescription(LocalizedString description) + { + ModDescription = description; + return this; } /// @@ -108,6 +201,47 @@ public SettingsBuilder AddImage(Sprite sprite, int height, float imageScale) return AddImageInternal(sprite, height, imageScale); } + /// + /// Adds mod's version to the mod description shown when you hover the mouse over the mod entry in the ModMenu dropdown + /// + /// + /// + /// Will be displayed only if you at least provided a Localized mod name with . + /// Mod version from the source mod info (if set with + /// or with ) will have higher precedence + public SettingsBuilder SetModVersion(string version) + { + Version = version; + return this; + } + + /// + /// Adds author's name mod description shown when you hover the mouse over the mod entry in the ModMenu dropdown + /// + /// + /// Will be displayed only if you at least provided a Localized mod name with . + /// Author's name from the source mod info (if set with + /// or with ) will have higher precedence. + public SettingsBuilder SetModAuthor(string author) + { + Author = author; + return this; + } + + /// + /// Adds an image to the mod description shown when you hover the mouse over the mod entry in the ModMenu dropdown + /// + /// + /// + /// Will be displayed only if you have a source mod info (if set with + /// or with ) or at least provided a Localized mod name with . + /// + public SettingsBuilder SetModIllustration(Sprite image) + { + modIllustration = image; + return this; + } + /// /// Adds a row containing an image. There is no setting tied to this, it is just for decoration. /// @@ -281,10 +415,19 @@ private SettingsBuilder Add(string key, ISettingsEntity entity, UISettingsEntity return this; } - internal (UISettingsGroup group, Dictionary settings) Build() + internal (List groups, Dictionary settings, Info info) Build() { Group.SettingsList = Settings.ToArray(); - return (Group, SettingsEntities); + Info Info; + if (Mod is OwlcatModification OwlMod) + Info = new(OwlMod, AllowDisabling, ModName, ModDescription, modIllustration); + else if (Mod is UnityModManager.ModEntry UMMmod) + Info = new(UMMmod, AllowDisabling, ModName, ModDescription, modIllustration); + else if (ModName is not null) + Info = new(ModName, ModDescription, Version, Author, modIllustration); + else + Info = new(GroupList.ElementAt(0)?.Title ?? ""); + return (GroupList, SettingsEntities, Info); } private void OpenDefaultSettingsDialog() @@ -319,8 +462,11 @@ private LocalizedString DefaultDescription() return Helpers.CreateString( $"mod-menu.default-description.{Group.name}", - $"Restore all settings in {Group.Title} to their defaults", - ruRU: $"Вернуть все настройки в группе {Group.Title} к значениям по умолчанию"); + enGB: $"Restore all settings in {Group.Title} to their defaults", + ruRU: $"Вернуть все настройки в группе {Group.Title} к значениям по умолчанию", + zhCN: $"还原所有{Group.Title}中的设置到默认值", + deDE: $"Setze alle Einstellungen in {Group.Title} auf ihre Standardwerte zurück", + frFR: "Rétablir les valeurs par défaut de tous les paramètres sous {Group.Title}"); } private LocalizedString DefaultDescriptionLong() @@ -328,13 +474,20 @@ private LocalizedString DefaultDescriptionLong() return Helpers.CreateString( $"mod-menu.default-description-long.{Group.name}", - $"Sets each settings under {Group.Title} to its default value. Your current settings will be lost." + enGB: $"Sets each settings under {Group.Title} to its default value. Your current settings will be lost." + $" Settings in other groups are not affected. Keep in mind this will apply to sub-groups under" + $" {Group.Title} as well (anything that is hidden when the group is collapsed).", ruRU: $"При нажатии на кнопку все настройки в группе {Group.Title} примут значения по умолчанию." + $" Ваши текущие настройки будут потеряны. Настройки из других групп затронуты не будут. Обратите внимание," + $" что изменения коснутся в том числе настроек из подгрупп, вложенных в {Group.Title}" + - $" (т.е. все те настройки, которые оказываются скрыты, когда вы сворачиваете группу)."); + $" (т.е. все те настройки, которые оказываются скрыты, когда вы сворачиваете группу).", + zhCN: $"{Group.Title}之中每一项设置的值都会变成各自的默认值。你的当前设置会丢失。其它分组的设置不受影响。" + + $"注意这也会影响{Group.Title}内部的小分组(只要是折叠之后看不见的都会影响.", + deDE: "Setzt alle Einstellungen unter {Group.Title} auf ihre Standardwerte zurück. Die aktuellen Einstellungen gehen dabei verloren. " + + "Einstellungen in anderen Gruppen werden nicht beeinflusst. Beachte, dass dies auch die Untergruppen von {Group.Title} betrifft.", + frFR: "Rétablit la valeur par défaut pour tous les paramètres sous {Group.Title}. Vos paramètres actuels vont être perdus. " + + "Les paramètres des autres sections ne seront pas affectés. Gardez en tête que cela va s'appliquer aussi aux sous-sections de {Group.Title} " + + "(tout ce qui est caché quand la section est réduite)"); } private static LocalizedString _defaultButtonLabel; @@ -343,7 +496,7 @@ private static LocalizedString DefaultButtonLabel get { _defaultButtonLabel ??= Helpers.CreateString( - "mod-menu.default-button-label", "Default", ruRU: "По умолчанию"); + "mod-menu.default-button-label", "Default", ruRU: "По умолчанию", zhCN: "默认值", deDE: "Standard", frFR: "Défaut"); return _defaultButtonLabel; } } @@ -448,6 +601,7 @@ public abstract class BaseSettingWithValue protected readonly string Key; protected readonly T DefaultValue; + /// /// Currently this is unused but I might add some kind of special handling later so the code is here. /// diff --git a/ModMenu/Settings/TestSettings.cs b/ModMenu/Settings/TestSettings.cs index afad8cc..f67bd61 100644 --- a/ModMenu/Settings/TestSettings.cs +++ b/ModMenu/Settings/TestSettings.cs @@ -31,6 +31,13 @@ internal void Initialize() { ModMenu.AddSettings( SettingsBuilder.New(RootKey, CreateString("title", "Testing settings")) + .SetMod(Main.Entry) + .SetModDescription(Helpers.CreateString("test-settings-desc", + enGB:"This is a test description for mod and let's make it a bit longer to take several lines.", + ruRU: "Здесь описание для тестового мода и пусть оно будь достаточно длинным, чтобы занимать пару строчек", + zhCN: "这是模组描述的一个测试案例,现在让我们多水一点字数吧,这样的话描述就能有好几行了", + deDE: "Dies ist eine Testbeschreibung für einen Mod, und sie ist noch ein wenig länger, damit sie mehrere Zeilen benötigt.", + frFR: "Ceci est un exemple de description de mod qui doit être assez long pour prendre deux lignes mais on n'a pas dit au traducteur quelle est la taille des lignes, j'imagine que c'est assez long maintenant. ")) .AddImage(Helpers.CreateSprite("ModMenu.WittleWolfie.png"), 250) .AddDefaultButton(OnDefaultsApplied) .AddButton( @@ -91,10 +98,8 @@ internal void Initialize() CreateString("int-desc", "This is a custom int slider"), minValue: 1, maxValue: 6) - .HideValueText())); - - ModMenu.AddSettings( - SettingsBuilder.New(GetKey("extra"), CreateString("extra-title", "More Test Settings")) + .HideValueText()) + .AddAnotherSettingsGroup(GetKey("extra"), CreateString("extra-title", "More Test Settings")) .AddDefaultButton() .AddToggle( Toggle.New(