From d0a2345e82c65acbd4bb32cca9a39c33ddb7f876 Mon Sep 17 00:00:00 2001 From: aedenthorn Date: Fri, 26 Mar 2021 10:10:56 -0400 Subject: [PATCH] etc --- AutoFuel/AedenthornUtils.cs | 11 + AutoStore/AedenthornUtils.cs | 11 + BuildingDemolish/AedenthornUtils.cs | 11 + ClockMod/AedenthornUtils.cs | 11 + ConfigurationManager/ConfigSettingEntry.cs | 76 ++ ConfigurationManager/ConfigurationManager.cs | 790 ++++++++++++++++++ .../ConfigurationManager.csproj | 116 +++ ConfigurationManager/LICENSE | 165 ++++ ConfigurationManager/LegacySettingEntry.cs | 131 +++ .../Properties/AssemblyInfo.cs | 36 + ConfigurationManager/SettingEntryBase.cs | 188 +++++ ConfigurationManager/SettingFieldDrawer.cs | 438 ++++++++++ ConfigurationManager/SettingSearcher.cs | 103 +++ ConfigurationManager/Utilities/ComboBox.cs | 138 +++ ConfigurationManager/Utilities/Utilities.cs | 146 ++++ ConfigurationManager/ValueChangedEventArgs.cs | 24 + ContainersAnywhere/AedenthornUtils.cs | 13 +- CustomAudio/CustomAudio.csproj | 3 +- CustomToolbarHotkeys/AedenthornUtils.cs | 33 + CustomToolbarHotkeys/BepInExPlugin.cs | 20 +- .../CustomToolbarHotkeys.csproj | 1 + SleepWithoutSpawn/AedenthornUtils.cs | 6 +- ValheimMods.sln | 6 + 23 files changed, 2462 insertions(+), 15 deletions(-) create mode 100644 ConfigurationManager/ConfigSettingEntry.cs create mode 100644 ConfigurationManager/ConfigurationManager.cs create mode 100644 ConfigurationManager/ConfigurationManager.csproj create mode 100644 ConfigurationManager/LICENSE create mode 100644 ConfigurationManager/LegacySettingEntry.cs create mode 100644 ConfigurationManager/Properties/AssemblyInfo.cs create mode 100644 ConfigurationManager/SettingEntryBase.cs create mode 100644 ConfigurationManager/SettingFieldDrawer.cs create mode 100644 ConfigurationManager/SettingSearcher.cs create mode 100644 ConfigurationManager/Utilities/ComboBox.cs create mode 100644 ConfigurationManager/Utilities/Utilities.cs create mode 100644 ConfigurationManager/ValueChangedEventArgs.cs create mode 100644 CustomToolbarHotkeys/AedenthornUtils.cs diff --git a/AutoFuel/AedenthornUtils.cs b/AutoFuel/AedenthornUtils.cs index db0dafd..f8b100f 100644 --- a/AutoFuel/AedenthornUtils.cs +++ b/AutoFuel/AedenthornUtils.cs @@ -19,4 +19,15 @@ public static bool CheckKeyDown(string value) return false; } } + public static bool CheckKeyHeld(string value, bool req = true) + { + try + { + return Input.GetKey(value.ToLower()); + } + catch + { + return !req; + } + } } diff --git a/AutoStore/AedenthornUtils.cs b/AutoStore/AedenthornUtils.cs index db0dafd..f8b100f 100644 --- a/AutoStore/AedenthornUtils.cs +++ b/AutoStore/AedenthornUtils.cs @@ -19,4 +19,15 @@ public static bool CheckKeyDown(string value) return false; } } + public static bool CheckKeyHeld(string value, bool req = true) + { + try + { + return Input.GetKey(value.ToLower()); + } + catch + { + return !req; + } + } } diff --git a/BuildingDemolish/AedenthornUtils.cs b/BuildingDemolish/AedenthornUtils.cs index db0dafd..f8b100f 100644 --- a/BuildingDemolish/AedenthornUtils.cs +++ b/BuildingDemolish/AedenthornUtils.cs @@ -19,4 +19,15 @@ public static bool CheckKeyDown(string value) return false; } } + public static bool CheckKeyHeld(string value, bool req = true) + { + try + { + return Input.GetKey(value.ToLower()); + } + catch + { + return !req; + } + } } diff --git a/ClockMod/AedenthornUtils.cs b/ClockMod/AedenthornUtils.cs index db0dafd..f8b100f 100644 --- a/ClockMod/AedenthornUtils.cs +++ b/ClockMod/AedenthornUtils.cs @@ -19,4 +19,15 @@ public static bool CheckKeyDown(string value) return false; } } + public static bool CheckKeyHeld(string value, bool req = true) + { + try + { + return Input.GetKey(value.ToLower()); + } + catch + { + return !req; + } + } } diff --git a/ConfigurationManager/ConfigSettingEntry.cs b/ConfigurationManager/ConfigSettingEntry.cs new file mode 100644 index 0000000..0aea166 --- /dev/null +++ b/ConfigurationManager/ConfigSettingEntry.cs @@ -0,0 +1,76 @@ +// Based on code made by MarC0 / ManlyMarco +// Copyright 2018 GNU General Public License v3.0 + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using BepInEx; +using BepInEx.Configuration; + +namespace ConfigurationManager +{ + internal sealed class ConfigSettingEntry : SettingEntryBase + { + public ConfigEntryBase Entry { get; } + + public ConfigSettingEntry(ConfigEntryBase entry, BaseUnityPlugin owner) + { + Entry = entry; + + DispName = entry.Definition.Key; + Category = entry.Definition.Section; + Description = entry.Description?.Description; + + var converter = TomlTypeConverter.GetConverter(entry.SettingType); + if (converter != null) + { + ObjToStr = o => converter.ConvertToString(o, entry.SettingType); + StrToObj = s => converter.ConvertToObject(s, entry.SettingType); + } + + var values = entry.Description?.AcceptableValues; + if (values != null) + GetAcceptableValues(values); + + DefaultValue = entry.DefaultValue; + + SetFromAttributes(entry.Description?.Tags, owner); + } + + private void GetAcceptableValues(AcceptableValueBase values) + { + var t = values.GetType(); + var listProp = t.GetProperty(nameof(AcceptableValueList.AcceptableValues), BindingFlags.Instance | BindingFlags.Public); + if (listProp != null) + { + AcceptableValues = ((IEnumerable)listProp.GetValue(values, null)).Cast().ToArray(); + } + else + { + var minProp = t.GetProperty(nameof(AcceptableValueRange.MinValue), BindingFlags.Instance | BindingFlags.Public); + if (minProp != null) + { + var maxProp = t.GetProperty(nameof(AcceptableValueRange.MaxValue), BindingFlags.Instance | BindingFlags.Public); + if (maxProp == null) throw new ArgumentNullException(nameof(maxProp)); + AcceptableValueRange = new KeyValuePair(minProp.GetValue(values, null), maxProp.GetValue(values, null)); + ShowRangeAsPercent = (AcceptableValueRange.Key.Equals(0) || AcceptableValueRange.Key.Equals(1)) && AcceptableValueRange.Value.Equals(100) || + AcceptableValueRange.Key.Equals(0f) && AcceptableValueRange.Value.Equals(1f); + } + } + } + + public override Type SettingType => Entry.SettingType; + + public override object Get() + { + return Entry.BoxedValue; + } + + protected override void SetValue(object newVal) + { + Entry.BoxedValue = newVal; + } + } +} \ No newline at end of file diff --git a/ConfigurationManager/ConfigurationManager.cs b/ConfigurationManager/ConfigurationManager.cs new file mode 100644 index 0000000..d07ccd9 --- /dev/null +++ b/ConfigurationManager/ConfigurationManager.cs @@ -0,0 +1,790 @@ +// Based on code made by MarC0 / ManlyMarco +// Copyright 2018 GNU General Public License v3.0 + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reflection; +using BepInEx; +using BepInEx.Logging; +using UnityEngine; +using BepInEx.Configuration; +using HarmonyLib; + +namespace ConfigurationManager +{ + /// + /// An easy way to let user configure how a plugin behaves without the need to make your own GUI. The user can change any of the settings you expose, even keyboard shortcuts. + /// https://github.com/ManlyMarco/BepInEx.ConfigurationManager + /// + [BepInPlugin(GUID, "Valheim Configuration Manager", Version)] + public class ConfigurationManager : BaseUnityPlugin + { + /// + /// GUID of this plugin + /// + public const string GUID = "aedenthorn.ConfigurationManager"; + private static bool isDebug = true; + public static void Dbgl(string str = "", bool pref = true) + { + if (isDebug) + Debug.Log((pref ? typeof(ConfigurationManager).Namespace + " " : "") + str); + } + /// + /// Version constant + /// + public const string Version = "0.1.0"; + private static ConfigurationManager context; + internal static new ManualLogSource Logger; + private static SettingFieldDrawer _fieldDrawer; + + private const int WindowId = -68; + + private const string SearchBoxName = "searchBox"; + private bool _focusSearchBox; + private string _searchString = string.Empty; + + /// + /// Event fired every time the manager window is shown or hidden. + /// + public event EventHandler> DisplayingWindowChanged; + + /// + /// Disable the hotkey check used by config manager. If enabled you have to set to show the manager. + /// + public bool OverrideHotkey; + + private bool _displayingWindow; + private bool _obsoleteCursor; + + private string _modsWithoutSettings; + + private List _allSettings; + private List _filteredSetings = new List(); + + internal Rect DefaultWindowRect { get; private set; } + private Rect _screenRect; + private Rect currentWindowRect; + private Vector2 _settingWindowScrollPos; + private int _tipsHeight; + private bool _showDebug; + + private PropertyInfo _curLockState; + private PropertyInfo _curVisible; + private int _previousCursorLockState; + private bool _previousCursorVisible; + + internal static Texture2D WindowBackground { get; private set; } + internal static Texture2D EntryBackground { get; private set; } + internal static Texture2D WidgetBackground { get; private set; } + + internal int LeftColumnWidth { get; private set; } + internal int RightColumnWidth { get; private set; } + + public static ConfigEntry _showAdvanced; + public static ConfigEntry _showKeybinds; + public static ConfigEntry _showSettings; + public static ConfigEntry _keybind; + public static ConfigEntry _hideSingleSection; + public static ConfigEntry _pluginConfigCollapsedDefault; + public static ConfigEntry _windowPosition; + public static ConfigEntry _windowSize; + public static ConfigEntry _textSize; + public static ConfigEntry _windowBackgroundColor; + public static ConfigEntry _entryBackgroundColor; + public static ConfigEntry _fontColor; + public static ConfigEntry _widgetBackgroundColor; + public static ConfigEntry nexusID; + + + public static GUIStyle windowStyle; + public static GUIStyle headerStyle; + public static GUIStyle entryStyle; + public static GUIStyle labelStyle; + public static GUIStyle toggleStyle; + public static GUIStyle buttonStyle; + public static GUIStyle boxStyle; + public static GUIStyle sliderStyle; + public static GUIStyle thumbStyle; + public static GUIStyle categoryHeaderSkin; + public static GUIStyle pluginHeaderSkin; + public static int fontSize = 14; + + /// + public ConfigurationManager() + { + context = this; + Logger = base.Logger; + CalculateDefaultWindowRect(); + _fieldDrawer = new SettingFieldDrawer(this); + + _keybind = Config.Bind("General", "Show config manager", new KeyboardShortcut(KeyCode.F1), + new ConfigDescription("The shortcut used to toggle the config manager window on and off.\n" + + "The key can be overridden by a game-specific plugin if necessary, in that case this setting is ignored.")); + nexusID = Config.Bind("General", "NexusID", 740, "Nexus mod ID for updates"); + + _showAdvanced = Config.Bind("Filtering", "Show advanced", true); + _showKeybinds = Config.Bind("Filtering", "Show keybinds", true); + _showSettings = Config.Bind("Filtering", "Show settings", true); + _hideSingleSection = Config.Bind("General", "Hide single sections", false, new ConfigDescription("Show section title for plugins with only one section")); + _pluginConfigCollapsedDefault = Config.Bind("General", "Plugin collapsed default", true, new ConfigDescription("If set to true plugins will be collapsed when opening the configuration manager window")); + _windowPosition = Config.Bind("General", "WindowPosition", new Vector2(55,35), "Window position"); + _windowSize = Config.Bind("General", "WindowSize", DefaultWindowRect.size, "Window size"); + _textSize = Config.Bind("General", "FontSize", 14, "Font Size"); + _windowBackgroundColor = Config.Bind("Colors", "WindowBackgroundColor", new Color(0,0,0,1), "Window background color"); + _entryBackgroundColor = Config.Bind("Colors", "EntryBackgroundColor", new Color(0.557f, 0.502f, 0.502f, 0.871f), "Etnry background color"); + _fontColor = Config.Bind("Colors", "FontColor", new Color(1, 0.714f, 0.361f, 1), "Font color"); + _widgetBackgroundColor = Config.Bind("Colors", "WidgetColor", new Color(0.882f, 0.463f, 0, 0.749f), "Widget color"); + + currentWindowRect = new Rect(_windowPosition.Value, _windowSize.Value); + Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), null); + + } + + + private void OnGUI() + { + if (DisplayingWindow) + { + if (Event.current.type == EventType.KeyUp && Event.current.keyCode == _keybind.Value.MainKey) + { + DisplayingWindow = false; + return; + } + + if(_textSize.Value > 9 && _textSize.Value < 100) + fontSize = Mathf.Clamp(_textSize.Value, 10, 30); + + CreateBackgrounds(); + CreateStyles(); + SetUnlockCursor(0, true); + + GUI.Box(currentWindowRect, GUIContent.none, new GUIStyle()); + GUI.backgroundColor = _windowBackgroundColor.Value; + + if(_windowSize.Value.x > 100 && _windowSize.Value.x < Screen.width && _windowSize.Value.y > 100 && _windowSize.Value.y < Screen.height) + currentWindowRect.size = _windowSize.Value; + + currentWindowRect = GUILayout.Window(WindowId, currentWindowRect, SettingsWindow, "Plugin / mod settings", windowStyle); + + if (!SettingFieldDrawer.SettingKeyboardShortcut) + Input.ResetInputAxes(); + + if (!Input.GetKey(KeyCode.Mouse0) && (currentWindowRect.x != _windowPosition.Value.x || currentWindowRect.y != _windowPosition.Value.y)) + { + _windowPosition.Value = currentWindowRect.position; + Config.Save(); + } + } + } + + private void SettingsWindow(int id) + { + GUI.DragWindow(new Rect(0, 0, currentWindowRect.width, 20)); + //DrawWindowHeader(); + + _settingWindowScrollPos = GUILayout.BeginScrollView(_settingWindowScrollPos, false, true); + + var scrollPosition = _settingWindowScrollPos.y; + var scrollHeight = currentWindowRect.height; + + GUILayout.BeginVertical(); + { + if (string.IsNullOrEmpty(SearchString)) + { + DrawTips(); + + if (_tipsHeight == 0 && Event.current.type == EventType.Repaint) + _tipsHeight = (int)GUILayoutUtility.GetLastRect().height; + } + + var currentHeight = _tipsHeight; + + foreach (var plugin in _filteredSetings) + { + var visible = plugin.Height == 0 || currentHeight + plugin.Height >= scrollPosition && currentHeight <= scrollPosition + scrollHeight; + + if (visible) + { + try + { + DrawSinglePlugin(plugin); + } + catch (ArgumentException) + { + // Needed to avoid GUILayout: Mismatched LayoutGroup.Repaint crashes on large lists + } + + if (plugin.Height == 0 && Event.current.type == EventType.Repaint) + plugin.Height = (int)GUILayoutUtility.GetLastRect().height; + } + else + { + try + { + GUILayout.Space(plugin.Height); + } + catch (ArgumentException) + { + // Needed to avoid GUILayout: Mismatched LayoutGroup.Repaint crashes on large lists + } + } + + currentHeight += plugin.Height; + } + + if (_showDebug) + { + GUILayout.Space(10); + GUILayout.Label("Plugins with no options available: " + _modsWithoutSettings, labelStyle); + } + else + { + // Always leave some space in case there's a dropdown box at the very bottom of the list + GUILayout.Space(70); + } + } + GUILayout.EndVertical(); + GUILayout.EndScrollView(); + + if (!SettingFieldDrawer.DrawCurrentDropdown()) + DrawTooltip(currentWindowRect); + } + + private void DrawTips() + { + GUILayout.BeginHorizontal(); + { + GUILayout.Label("Tip: Click plugin names to expand. Click setting and group names to see their descriptions.", labelStyle); + + GUILayout.FlexibleSpace(); + + Color color = GUI.backgroundColor; + GUI.backgroundColor = _widgetBackgroundColor.Value; + if (GUILayout.Button(_pluginConfigCollapsedDefault.Value ? "Expand" : "Collapse", buttonStyle, GUILayout.ExpandWidth(false))) + { + var newValue = !_pluginConfigCollapsedDefault.Value; + _pluginConfigCollapsedDefault.Value = newValue; + foreach (var plugin in _filteredSetings) + plugin.Collapsed = newValue; + } + GUI.backgroundColor = color; + } + GUILayout.EndHorizontal(); + } + + private void DrawWindowHeader() + { + GUI.backgroundColor = _entryBackgroundColor.Value; + GUILayout.BeginHorizontal(); + { + GUILayout.Label("Show: ", labelStyle, GUILayout.ExpandWidth(false)); + + GUI.enabled = SearchString == string.Empty; + + var newVal = GUILayout.Toggle(_showSettings.Value, "Normal settings", toggleStyle); + if (_showSettings.Value != newVal) + { + _showSettings.Value = newVal; + BuildFilteredSettingList(); + } + + newVal = GUILayout.Toggle(_showKeybinds.Value, "Keyboard shortcuts", toggleStyle); + if (_showKeybinds.Value != newVal) + { + _showKeybinds.Value = newVal; + BuildFilteredSettingList(); + } + + newVal = GUILayout.Toggle(_showAdvanced.Value, "Advanced settings", toggleStyle); + if (_showAdvanced.Value != newVal) + { + _showAdvanced.Value = newVal; + BuildFilteredSettingList(); + } + + GUI.enabled = true; + + newVal = GUILayout.Toggle(_showDebug, "Debug mode", toggleStyle); + if (_showDebug != newVal) + { + _showDebug = newVal; + BuildSettingList(); + } + + if (GUILayout.Button("Log", buttonStyle, GUILayout.ExpandWidth(false))) + { + try { Utilities.Utils.OpenLog(); } + catch (SystemException ex) { Logger.Log(LogLevel.Message | LogLevel.Error, ex.Message); } + } + } + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + { + GUILayout.Label("Search settings: ", labelStyle, GUILayout.ExpandWidth(false)); + + GUI.SetNextControlName(SearchBoxName); + SearchString = GUILayout.TextField(SearchString, GUILayout.ExpandWidth(true)); + + if (_focusSearchBox) + { + GUI.FocusWindow(WindowId); + GUI.FocusControl(SearchBoxName); + _focusSearchBox = false; + } + Color color = GUI.backgroundColor; + GUI.backgroundColor = _widgetBackgroundColor.Value; + if (GUILayout.Button("Clear", buttonStyle, GUILayout.ExpandWidth(false))) + SearchString = string.Empty; + GUI.backgroundColor = color; + } + GUILayout.EndHorizontal(); + } + + private void DrawSinglePlugin(PluginSettingsData plugin) + { + + var style = new GUIStyle(GUI.skin.box); + style.normal.textColor = _fontColor.Value; + style.normal.background = EntryBackground; + style.fontSize = fontSize; + GUI.backgroundColor = _entryBackgroundColor.Value; + + GUILayout.BeginVertical(style); + + var categoryHeader = _showDebug ? + new GUIContent(plugin.Info.Name.TrimStart('!')+" "+plugin.Info.Version, "GUID: " + plugin.Info.GUID) : + new GUIContent(plugin.Info.Name.TrimStart('!')+" "+plugin.Info.Version); + + var isSearching = !string.IsNullOrEmpty(SearchString); + + if (SettingFieldDrawer.DrawPluginHeader(categoryHeader, plugin.Collapsed && !isSearching) && !isSearching) + plugin.Collapsed = !plugin.Collapsed; + + if (isSearching || !plugin.Collapsed) + { + foreach (var category in plugin.Categories) + { + if (!string.IsNullOrEmpty(category.Name)) + { + if (plugin.Categories.Count > 1 || !_hideSingleSection.Value) + SettingFieldDrawer.DrawCategoryHeader(category.Name); + } + + foreach (var setting in category.Settings) + { + DrawSingleSetting(setting); + GUILayout.Space(2); + } + } + } + + GUILayout.EndVertical(); + } + + private void DrawSingleSetting(SettingEntryBase setting) + { + GUILayout.BeginHorizontal(); + { + try + { + DrawSettingName(setting); + _fieldDrawer.DrawSettingValue(setting); + DrawDefaultButton(setting); + } + catch (Exception ex) + { + Logger.Log(LogLevel.Error, $"Failed to draw setting {setting.DispName} - {ex}"); + GUILayout.Label("Failed to draw this field, check log for details.", labelStyle); + } + } + GUILayout.EndHorizontal(); + } + + private void DrawSettingName(SettingEntryBase setting) + { + if (setting.HideSettingName) return; + + + GUILayout.Label(new GUIContent(setting.DispName.TrimStart('!'), setting.Description), labelStyle, + GUILayout.Width(LeftColumnWidth), GUILayout.MaxWidth(LeftColumnWidth)); + + } + + private static void DrawDefaultButton(SettingEntryBase setting) + { + if (setting.HideDefaultButton) return; + + GUI.backgroundColor = _widgetBackgroundColor.Value; + + bool DrawDefaultButton() + { + GUILayout.Space(5); + return GUILayout.Button("Reset", buttonStyle, GUILayout.ExpandWidth(false)); + } + + if (setting.DefaultValue != null) + { + if (DrawDefaultButton()) + setting.Set(setting.DefaultValue); + } + else if (setting.SettingType.IsClass) + { + if (DrawDefaultButton()) + setting.Set(null); + } + } + + /// + /// Is the config manager main window displayed on screen + /// + public bool DisplayingWindow + { + get => _displayingWindow; + set + { + if (_displayingWindow == value) return; + _displayingWindow = value; + + SettingFieldDrawer.ClearCache(); + + if (_displayingWindow) + { + CalculateDefaultWindowRect(); + + BuildSettingList(); + + _focusSearchBox = true; + + // Do through reflection for unity 4 compat + if (_curLockState != null) + { + _previousCursorLockState = _obsoleteCursor ? Convert.ToInt32((bool)_curLockState.GetValue(null, null)) : (int)_curLockState.GetValue(null, null); + _previousCursorVisible = (bool)_curVisible.GetValue(null, null); + } + } + else + { + if (!_previousCursorVisible || _previousCursorLockState != 0) // 0 = CursorLockMode.None + SetUnlockCursor(_previousCursorLockState, _previousCursorVisible); + } + + DisplayingWindowChanged?.Invoke(this, new ValueChangedEventArgs(value)); + } + } + + /// + /// Register a custom setting drawer for a given type. The action is ran in OnGui in a single setting slot. + /// Do not use any Begin / End layout methods, and avoid raising height from standard. + /// + public static void RegisterCustomSettingDrawer(Type settingType, Action onGuiDrawer) + { + if (settingType == null) throw new ArgumentNullException(nameof(settingType)); + if (onGuiDrawer == null) throw new ArgumentNullException(nameof(onGuiDrawer)); + + if (SettingFieldDrawer.SettingDrawHandlers.ContainsKey(settingType)) + Logger.LogWarning("Tried to add a setting drawer for type " + settingType.FullName + " while one already exists."); + else + SettingFieldDrawer.SettingDrawHandlers[settingType] = onGuiDrawer; + } + + public void BuildSettingList() + { + SettingSearcher.CollectSettings(out var results, out var modsWithoutSettings, _showDebug); + + _modsWithoutSettings = string.Join(", ", modsWithoutSettings.Select(x => x.TrimStart('!')).OrderBy(x => x).ToArray()); + _allSettings = results.ToList(); + + BuildFilteredSettingList(); + } + + private void BuildFilteredSettingList() + { + IEnumerable results = _allSettings; + + var searchStrings = SearchString.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + if (searchStrings.Length > 0) + { + results = results.Where(x => ContainsSearchString(x, searchStrings)); + } + else + { + if (!_showAdvanced.Value) + results = results.Where(x => x.IsAdvanced != true); + if (!_showKeybinds.Value) + results = results.Where(x => !IsKeyboardShortcut(x)); + if (!_showSettings.Value) + results = results.Where(x => x.IsAdvanced == true || IsKeyboardShortcut(x)); + } + + const string shortcutsCatName = "Keyboard shortcuts"; + string GetCategory(SettingEntryBase eb) + { + + return eb.Category; + } + + var settingsAreCollapsed = _pluginConfigCollapsedDefault.Value; + + var nonDefaultCollpasingStateByPluginName = new HashSet(); + foreach (var pluginSetting in _filteredSetings) + { + if (pluginSetting.Collapsed != settingsAreCollapsed) + { + nonDefaultCollpasingStateByPluginName.Add(pluginSetting.Info.Name); + } + } + + _filteredSetings = results + .GroupBy(x => x.PluginInfo) + .Select(pluginSettings => + { + var categories = pluginSettings + .GroupBy(GetCategory) + .OrderBy(x => string.Equals(x.Key, shortcutsCatName, StringComparison.Ordinal)) + .ThenBy(x => x.Key) + .Select(x => new PluginSettingsData.PluginSettingsGroupData { Name = x.Key, Settings = x.OrderByDescending(set => set.Order).ThenBy(set => set.DispName).ToList() }); + + return new PluginSettingsData { Info = pluginSettings.Key, Categories = categories.ToList(), Collapsed = nonDefaultCollpasingStateByPluginName.Contains(pluginSettings.Key.Name) ? !settingsAreCollapsed : settingsAreCollapsed }; + }) + .OrderBy(x => x.Info.Name) + .ToList(); + } + + private static bool IsKeyboardShortcut(SettingEntryBase x) + { + return x.SettingType == typeof(BepInEx.Configuration.KeyboardShortcut); + } + + private static bool ContainsSearchString(SettingEntryBase setting, string[] searchStrings) + { + var combinedSearchTarget = setting.PluginInfo.Name + "\n" + + setting.PluginInfo.GUID + "\n" + + setting.DispName + "\n" + + setting.Category + "\n" + + setting.Description + "\n" + + setting.DefaultValue + "\n" + + setting.Get(); + + return searchStrings.All(s => combinedSearchTarget.IndexOf(s, StringComparison.InvariantCultureIgnoreCase) >= 0); + } + + private void CalculateDefaultWindowRect() + { + var width = Mathf.Min(Screen.width, 650); + var height = Screen.height < 560 ? Screen.height : Screen.height - 100; + var offsetX = Mathf.RoundToInt((Screen.width - width) / 2f); + var offsetY = Mathf.RoundToInt((Screen.height - height) / 2f); + DefaultWindowRect = new Rect(offsetX, offsetY, width, height); + + _screenRect = new Rect(0, 0, Screen.width, Screen.height); + + LeftColumnWidth = Mathf.RoundToInt(DefaultWindowRect.width / 2.5f); + RightColumnWidth = (int)DefaultWindowRect.width - LeftColumnWidth - 115; + } + + private static void DrawTooltip(Rect area) + { + if (!string.IsNullOrEmpty(GUI.tooltip)) + { + var currentEvent = Event.current; + + var style = new GUIStyle(boxStyle) + { + wordWrap = true, + alignment = TextAnchor.MiddleCenter + }; + var color = GUI.backgroundColor; + GUI.backgroundColor = _entryBackgroundColor.Value; + const int width = 400; + var height = style.CalcHeight(new GUIContent(GUI.tooltip), 400) + 10; + + var x = currentEvent.mousePosition.x + width > area.width + ? area.width - width + : currentEvent.mousePosition.x; + + var y = currentEvent.mousePosition.y + 25 + height > area.height + ? currentEvent.mousePosition.y - height + : currentEvent.mousePosition.y + 25; + + GUI.Box(new Rect(x, y, width, height), GUI.tooltip, style); + GUI.backgroundColor = color; + } + } + + + /// + /// String currently entered into the search box + /// + public string SearchString + { + get => _searchString; + private set + { + if (value == null) + value = string.Empty; + + if (_searchString == value) + return; + + _searchString = value; + + BuildFilteredSettingList(); + } + } + + private void Start() + { + // Use reflection to keep compatibility with unity 4.x since it doesn't have Cursor + var tCursor = typeof(Cursor); + _curLockState = tCursor.GetProperty("lockState", BindingFlags.Static | BindingFlags.Public); + _curVisible = tCursor.GetProperty("visible", BindingFlags.Static | BindingFlags.Public); + + if (_curLockState == null && _curVisible == null) + { + _obsoleteCursor = true; + + _curLockState = typeof(Screen).GetProperty("lockCursor", BindingFlags.Static | BindingFlags.Public); + _curVisible = typeof(Screen).GetProperty("showCursor", BindingFlags.Static | BindingFlags.Public); + } + + // Check if user has permissions to write config files to disk + try { Config.Save(); } + catch (IOException ex) { Logger.Log(LogLevel.Message | LogLevel.Warning, "WARNING: Failed to write to config directory, expect issues!\nError message:" + ex.Message); } + catch (UnauthorizedAccessException ex) { Logger.Log(LogLevel.Message | LogLevel.Warning, "WARNING: Permission denied to write to config directory, expect issues!\nError message:" + ex.Message); } + } + + private void Update() + { + if (DisplayingWindow) SetUnlockCursor(0, true); + + if (OverrideHotkey) return; + + if (!DisplayingWindow && _keybind.Value.IsUp()) + { + CreateBackgrounds(); + + DisplayingWindow = true; + } + } + + private void CreateStyles() + { + windowStyle = new GUIStyle(GUI.skin.window); + windowStyle.normal.textColor = _fontColor.Value; + //windowStyle.fontSize = fontSize; + windowStyle.active.textColor = _fontColor.Value; + + labelStyle = new GUIStyle(GUI.skin.label); + labelStyle.normal.textColor = _fontColor.Value; + labelStyle.fontSize = fontSize; + + buttonStyle = new GUIStyle(GUI.skin.button); + buttonStyle.normal.textColor = _fontColor.Value; + buttonStyle.fontSize = fontSize; + + categoryHeaderSkin = new GUIStyle(labelStyle) + { + alignment = TextAnchor.UpperCenter, + wordWrap = true, + stretchWidth = true, + }; + pluginHeaderSkin = new GUIStyle(categoryHeaderSkin); + + + + toggleStyle = new GUIStyle(GUI.skin.toggle); + toggleStyle.normal.textColor = _fontColor.Value; + toggleStyle.fontSize = fontSize; + + boxStyle = new GUIStyle(GUI.skin.box); + boxStyle.normal.textColor = _fontColor.Value; + boxStyle.fontSize = fontSize; + + sliderStyle = new GUIStyle(GUI.skin.horizontalSlider); + + thumbStyle = new GUIStyle(GUI.skin.horizontalSliderThumb); + } + private void CreateBackgrounds() + { + var background = new Texture2D(1, 1, TextureFormat.ARGB32, false); + background.SetPixel(0, 0, _windowBackgroundColor.Value); + background.Apply(); + WindowBackground = background; + + var entryBackground = new Texture2D(1, 1, TextureFormat.ARGB32, false); + entryBackground.SetPixel(0, 0, _entryBackgroundColor.Value); + entryBackground.Apply(); + EntryBackground = entryBackground; + } + + private void LateUpdate() + { + if (DisplayingWindow) SetUnlockCursor(0, true); + } + + private void SetUnlockCursor(int lockState, bool cursorVisible) + { + if (_curLockState != null) + { + // Do through reflection for unity 4 compat + //Cursor.lockState = CursorLockMode.None; + //Cursor.visible = true; + if(_obsoleteCursor) + _curLockState.SetValue(null, Convert.ToBoolean(lockState), null); + else + _curLockState.SetValue(null, lockState, null); + + _curVisible.SetValue(null, cursorVisible, null); + } + } + + private sealed class PluginSettingsData + { + public BepInPlugin Info; + public List Categories; + private bool _collapsed; + + public bool Collapsed + { + get => _collapsed; + set + { + _collapsed = value; + Height = 0; + } + } + + public sealed class PluginSettingsGroupData + { + public string Name; + public List Settings; + } + + public int Height { get; set; } + } + + [HarmonyPatch(typeof(Console), "InputText")] + static class InputText_Patch + { + static bool Prefix(Console __instance) + { + string text = __instance.m_input.text; + if (text.ToLower().Equals($"{typeof(ConfigurationManager).Namespace.ToLower()} reset")) + { + context.Config.Reload(); + context.Config.Save(); + Traverse.Create(__instance).Method("AddString", new object[] { text }).GetValue(); + Traverse.Create(__instance).Method("AddString", new object[] { $"{context.Info.Metadata.Name} config reloaded" }).GetValue(); + return false; + } + return true; + } + } + + } +} diff --git a/ConfigurationManager/ConfigurationManager.csproj b/ConfigurationManager/ConfigurationManager.csproj new file mode 100644 index 0000000..049fc36 --- /dev/null +++ b/ConfigurationManager/ConfigurationManager.csproj @@ -0,0 +1,116 @@ + + + + + Debug + AnyCPU + {104215E5-C277-4D43-A52C-B31467312299} + Library + Properties + ConfigurationManager + ConfigurationManager + v4.5.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\BepInEx\0Harmony20.dll + + + ..\valheim_Data\Managed\assembly_guiutils.dll + + + ..\valheim_Data\Managed\assembly_valheim.dll + + + ..\BepInEx\BepInEx.dll + + + + + + + + + + + ..\valheim_Data\Managed\UnityEngine.dll + + + ..\valheim_Data\Managed\UnityEngine.AnimationModule.dll + + + ..\valheim_Data\Managed\UnityEngine.AudioModule.dll + + + ..\valheim_Data\Managed\UnityEngine.CoreModule.dll + + + ..\valheim_Data\Managed\UnityEngine.ImageConversionModule.dll + + + ..\valheim_Data\Managed\UnityEngine.IMGUIModule.dll + + + ..\valheim_Data\Managed\UnityEngine.InputLegacyModule.dll + + + ..\valheim_Data\Managed\UnityEngine.InputModule.dll + + + ..\valheim_Data\Managed\UnityEngine.JSONSerializeModule.dll + + + ..\valheim_Data\Managed\UnityEngine.PhysicsModule.dll + + + ..\valheim_Data\Managed\UnityEngine.TextCoreModule.dll + + + ..\valheim_Data\Managed\UnityEngine.TextRenderingModule.dll + + + ..\valheim_Data\Managed\UnityEngine.UI.dll + + + ..\valheim_Data\Managed\UnityEngine.UIModule.dll + + + + + + + + + + + + + + + + + + + + call $(SolutionDir)copyDll.bat $(ProjectName) + + \ No newline at end of file diff --git a/ConfigurationManager/LICENSE b/ConfigurationManager/LICENSE new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/ConfigurationManager/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/ConfigurationManager/LegacySettingEntry.cs b/ConfigurationManager/LegacySettingEntry.cs new file mode 100644 index 0000000..a80a618 --- /dev/null +++ b/ConfigurationManager/LegacySettingEntry.cs @@ -0,0 +1,131 @@ +// Based on code made by MarC0 / ManlyMarco +// Copyright 2018 GNU General Public License v3.0 + +using BepInEx; +using BepInEx.Logging; +using System; +using System.Reflection; + +namespace ConfigurationManager +{ + internal class LegacySettingEntry : SettingEntryBase + { + private Type _settingType; + + private LegacySettingEntry() + { + } + + public override string DispName + { + get => string.IsNullOrEmpty(base.DispName) ? Property.Name : base.DispName; + protected internal set => base.DispName = value; + } + + public object Instance { get; internal set; } + public PropertyInfo Property { get; internal set; } + + public override Type SettingType => _settingType ?? (_settingType = Property.PropertyType); + + public override object Get() => Property.GetValue(Instance, null); + + protected override void SetValue(object newVal) => Property.SetValue(Instance, newVal, null); + + /// + /// Instance of the object that holds this setting. + /// Null if setting is not in a ConfigWrapper. + /// + public object Wrapper { get; internal set; } + + public static LegacySettingEntry FromConfigWrapper(object instance, PropertyInfo settingProp, + BepInPlugin pluginInfo, BaseUnityPlugin pluginInstance) + { + try + { + var wrapper = settingProp.GetValue(instance, null); + + if (wrapper == null) + { + ConfigurationManager.Logger.Log(LogLevel.Debug, $"Skipping ConfigWrapper entry because it's null : {instance} | {settingProp.Name} | {pluginInfo?.Name}"); + return null; + } + + var innerProp = wrapper.GetType().GetProperty("Value", BindingFlags.Instance | BindingFlags.Public); + + var entry = new LegacySettingEntry(); + entry.SetFromAttributes(settingProp.GetCustomAttributes(false), pluginInstance); + + if (innerProp == null) + { + ConfigurationManager.Logger.Log(LogLevel.Error, "Failed to find property Value of ConfigWrapper"); + return null; + } + + entry.Browsable = innerProp.CanRead && innerProp.CanWrite && entry.Browsable != false; + + entry.Property = innerProp; + entry.Instance = wrapper; + + entry.Wrapper = wrapper; + + if (entry.DispName == "Value") + entry.DispName = wrapper.GetType().GetProperty("Key", BindingFlags.Instance | BindingFlags.Public) + ?.GetValue(wrapper, null) as string; + + if (string.IsNullOrEmpty(entry.Category)) + { + var section = wrapper.GetType().GetProperty("Section", BindingFlags.Instance | BindingFlags.Public) + ?.GetValue(wrapper, null) as string; + if (section != pluginInfo?.GUID) + entry.Category = section; + } + + var strToObj = wrapper.GetType().GetField("_strToObj", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(wrapper); + if (strToObj != null) + { + var inv = strToObj.GetType().GetMethod("Invoke", BindingFlags.Instance | BindingFlags.Public); + if (inv != null) + entry.StrToObj = s => inv.Invoke(strToObj, new object[] { s }); + } + + var objToStr = wrapper.GetType().GetField("_objToStr", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(wrapper); + if (objToStr != null) + { + var inv = objToStr.GetType().GetMethod("Invoke", BindingFlags.Instance | BindingFlags.Public); + if (inv != null) + entry.ObjToStr = o => inv.Invoke(objToStr, new object[] { o }) as string; + } + else + { + entry.ObjToStr = o => o.ToString(); + } + + return entry; + } + catch (SystemException ex) + { + ConfigurationManager.Logger.Log(LogLevel.Error, + $"Failed to create ConfigWrapper entry : {instance} | {settingProp?.Name} | {pluginInfo?.Name} | Error: {ex.Message}"); + return null; + } + } + + public static LegacySettingEntry FromNormalProperty(object instance, PropertyInfo settingProp, + BepInPlugin pluginInfo, BaseUnityPlugin pluginInstance) + { + var entry = new LegacySettingEntry(); + entry.SetFromAttributes(settingProp.GetCustomAttributes(false), pluginInstance); + + if (entry.Browsable == null) + entry.Browsable = settingProp.CanRead && settingProp.CanWrite; + entry.ReadOnly = settingProp.CanWrite; + + entry.Property = settingProp; + entry.Instance = instance; + + return entry; + } + } +} \ No newline at end of file diff --git a/ConfigurationManager/Properties/AssemblyInfo.cs b/ConfigurationManager/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..ed66b4f --- /dev/null +++ b/ConfigurationManager/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ConfigurationManager")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ConfigurationManager")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("104215e5-c277-4d43-a52c-b31467312299")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/ConfigurationManager/SettingEntryBase.cs b/ConfigurationManager/SettingEntryBase.cs new file mode 100644 index 0000000..ba8b5ff --- /dev/null +++ b/ConfigurationManager/SettingEntryBase.cs @@ -0,0 +1,188 @@ +// Based on code made by MarC0 / ManlyMarco +// Copyright 2018 GNU General Public License v3.0 + +using BepInEx; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; + +namespace ConfigurationManager +{ + /// + /// Class representing all data about a setting collected by ConfigurationManager. + /// + public abstract class SettingEntryBase + { + /// + /// List of values this setting can take + /// + public object[] AcceptableValues { get; protected set; } + + /// + /// Range of the values this setting can take + /// + public KeyValuePair AcceptableValueRange { get; protected set; } + + /// + /// Should the setting be shown as a percentage (only applies to value range settings) + /// + public bool? ShowRangeAsPercent { get; protected set; } + + /// + /// Custom setting draw action + /// + public Action CustomDrawer { get; private set; } + + /// + /// Show this setting in the settings screen at all? If false, don't show. + /// + public bool? Browsable { get; protected set; } + + /// + /// Category the setting is under. Null to be directly under the plugin. + /// + public string Category { get; protected set; } + + /// + /// If set, a "Default" button will be shown next to the setting to allow resetting to default. + /// + public object DefaultValue { get; protected set; } + + /// + /// Force the "Reset" button to not be displayed, even if a valid DefaultValue is available. + /// + public bool HideDefaultButton { get; protected set; } + + /// + /// Force the setting name to not be displayed. Should only be used with a to get more space. + /// Can be used together with to gain even more space. + /// + public bool HideSettingName { get; protected set; } + + /// + /// Optional description shown when hovering over the setting + /// + public string Description { get; protected internal set; } + + /// + /// Name of the setting + /// + public virtual string DispName { get; protected internal set; } + + /// + /// Plugin this setting belongs to + /// + public BepInPlugin PluginInfo { get; protected internal set; } + + /// + /// Only allow showing of the value. False whenever possible by default. + /// + public bool? ReadOnly { get; protected set; } + + /// + /// Type of the variable + /// + public abstract Type SettingType { get; } + + /// + /// Instance of the plugin that owns this setting + /// + public BaseUnityPlugin PluginInstance { get; private set; } + + /// + /// Is this setting advanced + /// + public bool? IsAdvanced { get; internal set; } + + /// + /// Order of the setting on the settings list relative to other settings in a category. 0 by default, lower is higher on the list. + /// + public int Order { get; protected set; } + + /// + /// Get the value of this setting + /// + public abstract object Get(); + + /// + /// Set the value of this setting + /// + public void Set(object newVal) + { + if (ReadOnly != true) + SetValue(newVal); + } + + /// + /// Implementation of + /// + protected abstract void SetValue(object newVal); + + /// + /// Custom converter from setting type to string for the textbox + /// + public Func ObjToStr { get; internal set; } + + /// + /// Custom converter from string to setting type for the textbox + /// + public Func StrToObj { get; internal set; } + + private static readonly PropertyInfo[] _myProperties = typeof(SettingEntryBase).GetProperties(BindingFlags.Instance | BindingFlags.Public); + + internal void SetFromAttributes(object[] attribs, BaseUnityPlugin pluginInstance) + { + PluginInstance = pluginInstance; + PluginInfo = pluginInstance?.Info.Metadata; + + if (attribs == null || attribs.Length == 0) return; + + foreach (var attrib in attribs) + { + switch (attrib) + { + case null: break; + + // Obsolete attributes from early bepin5 ----------------------- + case Action newCustomDraw: + CustomDrawer = _ => newCustomDraw(this); + break; + case string str: + switch (str) + { + case "ReadOnly": ReadOnly = true; break; + case "Browsable": Browsable = true; break; + case "Unbrowsable": case "Hidden": Browsable = false; break; + case "Advanced": IsAdvanced = true; break; + } + break; + + // Copy attributes from a specially formatted object, currently recommended + default: + var attrType = attrib.GetType(); + if (attrType.Name == "ConfigurationManagerAttributes") + { + var otherFields = attrType.GetFields(BindingFlags.Instance | BindingFlags.Public); + foreach (var propertyPair in _myProperties.Join(otherFields, my => my.Name, other => other.Name, (my, other) => new { my, other })) + { + try + { + var val = propertyPair.other.GetValue(attrib); + if (val != null) + propertyPair.my.SetValue(this, val, null); + } + catch (Exception ex) + { + ConfigurationManager.Logger.LogWarning($"Failed to copy value {propertyPair.my.Name} from provided tag object {attrType.FullName} - " + ex.Message); + } + } + break; + } + return; + } + } + } + } +} diff --git a/ConfigurationManager/SettingFieldDrawer.cs b/ConfigurationManager/SettingFieldDrawer.cs new file mode 100644 index 0000000..d06e2ea --- /dev/null +++ b/ConfigurationManager/SettingFieldDrawer.cs @@ -0,0 +1,438 @@ +// Based on code made by MarC0 / ManlyMarco +// Copyright 2018 GNU General Public License v3.0 + +using ConfigurationManager.Utilities; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using UnityEngine; + +namespace ConfigurationManager +{ + internal class SettingFieldDrawer + { + private static readonly IEnumerable _keysToCheck = BepInEx.Configuration.KeyboardShortcut.AllKeyCodes.Except(new[] { KeyCode.Mouse0, KeyCode.None }).ToArray(); + + public static Dictionary> SettingDrawHandlers { get; } + + private static readonly Dictionary _comboBoxCache = new Dictionary(); + private static readonly Dictionary _colorCache = new Dictionary(); + + private readonly ConfigurationManager _instance; + + private static SettingEntryBase _currentKeyboardShortcutToSet; + public static bool SettingKeyboardShortcut => _currentKeyboardShortcutToSet != null; + + static SettingFieldDrawer() + { + SettingDrawHandlers = new Dictionary> + { + {typeof(bool), DrawBoolField}, + {typeof(BepInEx.Configuration.KeyboardShortcut), DrawKeyboardShortcut}, + {typeof(Color), DrawColor }, + {typeof(Vector2), DrawVector2 }, + {typeof(Vector3), DrawVector3 }, + {typeof(Vector4), DrawVector4 }, + {typeof(Quaternion), DrawQuaternion }, + }; + } + + public SettingFieldDrawer(ConfigurationManager instance) + { + _instance = instance; + } + + public void DrawSettingValue(SettingEntryBase setting) + { + GUI.backgroundColor = ConfigurationManager._widgetBackgroundColor.Value; + + if (setting.CustomDrawer != null) + setting.CustomDrawer(setting is ConfigSettingEntry newSetting ? newSetting.Entry : null); + else if (setting.ShowRangeAsPercent != null && setting.AcceptableValueRange.Key != null) + DrawRangeField(setting); + else if (setting.AcceptableValues != null) + DrawListField(setting); + else if (setting.SettingType.IsEnum) + { + if (setting.SettingType.GetCustomAttributes(typeof(FlagsAttribute), false).Any()) + DrawFlagsField(setting, Enum.GetValues(setting.SettingType), _instance.RightColumnWidth); + else + DrawComboboxField(setting, Enum.GetValues(setting.SettingType), _instance.DefaultWindowRect.yMax); + } + else + DrawFieldBasedOnValueType(setting); + } + + public static void ClearCache() + { + _comboBoxCache.Clear(); + + foreach (var tex in _colorCache) + UnityEngine.Object.Destroy(tex.Value.Tex); + _colorCache.Clear(); + } + + public static void DrawCenteredLabel(string text, params GUILayoutOption[] options) + { + GUILayout.BeginHorizontal(options); + GUILayout.FlexibleSpace(); + GUILayout.Label(text, ConfigurationManager.labelStyle); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + } + + public static void DrawCategoryHeader(string text) + { + GUILayout.Label(text, ConfigurationManager.categoryHeaderSkin); + } + + public static bool DrawPluginHeader(GUIContent content, bool isCollapsed) + { + + //if (isCollapsed) content.text += "\n..."; + return GUILayout.Button(content, ConfigurationManager.pluginHeaderSkin, GUILayout.ExpandWidth(true)); + } + + public static bool DrawCurrentDropdown() + { + if (ComboBox.CurrentDropdownDrawer != null) + { + ComboBox.CurrentDropdownDrawer.Invoke(); + ComboBox.CurrentDropdownDrawer = null; + return true; + } + return false; + } + + private void DrawListField(SettingEntryBase setting) + { + var acceptableValues = setting.AcceptableValues; + if (acceptableValues.Length == 0) + throw new ArgumentException("AcceptableValueListAttribute returned an empty list of acceptable values. You need to supply at least 1 option."); + + if (!setting.SettingType.IsInstanceOfType(acceptableValues.FirstOrDefault(x => x != null))) + throw new ArgumentException("AcceptableValueListAttribute returned a list with items of type other than the settng type itself."); + + DrawComboboxField(setting, acceptableValues, _instance.DefaultWindowRect.yMax); + } + + private void DrawFieldBasedOnValueType(SettingEntryBase setting) + { + if (SettingDrawHandlers.TryGetValue(setting.SettingType, out var drawMethod)) + drawMethod(setting); + else + DrawUnknownField(setting, _instance.RightColumnWidth); + } + + private static void DrawBoolField(SettingEntryBase setting) + { + GUI.backgroundColor = ConfigurationManager._widgetBackgroundColor.Value; + var boolVal = (bool)setting.Get(); + var result = GUILayout.Toggle(boolVal, boolVal ? "Enabled" : "Disabled", ConfigurationManager.toggleStyle, GUILayout.ExpandWidth(true)); + if (result != boolVal) + setting.Set(result); + } + + private static void DrawFlagsField(SettingEntryBase setting, IList enumValues, int maxWidth) + { + var currentValue = Convert.ToInt64(setting.Get()); + var allValues = enumValues.Cast().Select(x => new { name = x.ToString(), val = Convert.ToInt64(x) }).ToArray(); + + // Vertically stack Horizontal groups of the options to deal with the options taking more width than is available in the window + GUILayout.BeginVertical(GUILayout.MaxWidth(maxWidth)); + { + for (var index = 0; index < allValues.Length;) + { + GUILayout.BeginHorizontal(); + { + var currentWidth = 0; + for (; index < allValues.Length; index++) + { + var value = allValues[index]; + + // Skip the 0 / none enum value, just uncheck everything to get 0 + if (value.val != 0) + { + // Make sure this horizontal group doesn't extend over window width, if it does then start a new horiz group below + var textDimension = (int)GUI.skin.toggle.CalcSize(new GUIContent(value.name)).x; + currentWidth += textDimension; + if (currentWidth > maxWidth) + break; + + GUI.changed = false; + + + var newVal = GUILayout.Toggle((currentValue & value.val) == value.val, value.name, ConfigurationManager.toggleStyle, + GUILayout.ExpandWidth(false)); + if (GUI.changed) + { + var newValue = newVal ? currentValue | value.val : currentValue & ~value.val; + setting.Set(Enum.ToObject(setting.SettingType, newValue)); + } + } + } + } + GUILayout.EndHorizontal(); + } + + GUI.changed = false; + } + GUILayout.EndVertical(); + // Make sure the reset button is properly spaced + GUILayout.FlexibleSpace(); + } + + private static void DrawComboboxField(SettingEntryBase setting, IList list, float windowYmax) + { + var buttonText = ObjectToGuiContent(setting.Get()); + var dispRect = GUILayoutUtility.GetRect(buttonText, GUI.skin.button, GUILayout.ExpandWidth(true)); + + if (!_comboBoxCache.TryGetValue(setting, out var box)) + { + box = new ComboBox(dispRect, buttonText, list.Cast().Select(ObjectToGuiContent).ToArray(), ConfigurationManager.boxStyle, windowYmax); + _comboBoxCache[setting] = box; + } + else + { + box.Rect = dispRect; + box.ButtonContent = buttonText; + } + + box.Show(id => + { + if (id >= 0 && id < list.Count) + setting.Set(list[id]); + }); + } + + private static GUIContent ObjectToGuiContent(object x) + { + if (x is Enum) + { + var enumType = x.GetType(); + var enumMember = enumType.GetMember(x.ToString()).FirstOrDefault(); + var attr = enumMember?.GetCustomAttributes(typeof(DescriptionAttribute), false).Cast().FirstOrDefault(); + if (attr != null) + return new GUIContent(attr.Description); + return new GUIContent(x.ToString().ToProperCase()); + } + return new GUIContent(x.ToString()); + } + + private static void DrawRangeField(SettingEntryBase setting) + { + var value = setting.Get(); + var converted = (float)Convert.ToDouble(value, CultureInfo.InvariantCulture); + var leftValue = (float)Convert.ToDouble(setting.AcceptableValueRange.Key, CultureInfo.InvariantCulture); + var rightValue = (float)Convert.ToDouble(setting.AcceptableValueRange.Value, CultureInfo.InvariantCulture); + + var result = GUILayout.HorizontalSlider(converted, leftValue, rightValue, ConfigurationManager.sliderStyle, ConfigurationManager.thumbStyle, GUILayout.ExpandWidth(true)); + if (Math.Abs(result - converted) > Mathf.Abs(rightValue - leftValue) / 1000) + { + var newValue = Convert.ChangeType(result, setting.SettingType, CultureInfo.InvariantCulture); + setting.Set(newValue); + } + + if (setting.ShowRangeAsPercent == true) + { + DrawCenteredLabel( + Mathf.Round(100 * Mathf.Abs(result - leftValue) / Mathf.Abs(rightValue - leftValue)) + "%", + GUILayout.Width(50)); + } + else + { + var strVal = value.ToString().AppendZeroIfFloat(setting.SettingType); + var strResult = GUILayout.TextField(strVal, GUILayout.Width(50)); + if (strResult != strVal) + { + try + { + var resultVal = (float)Convert.ToDouble(strResult, CultureInfo.InvariantCulture); + var clampedResultVal = Mathf.Clamp(resultVal, leftValue, rightValue); + setting.Set(Convert.ChangeType(clampedResultVal, setting.SettingType, CultureInfo.InvariantCulture)); + } + catch (FormatException) + { + // Ignore user typing in bad data + } + } + } + } + + private void DrawUnknownField(SettingEntryBase setting, int rightColumnWidth) + { + // Try to use user-supplied converters + if (setting.ObjToStr != null && setting.StrToObj != null) + { + var text = setting.ObjToStr(setting.Get()).AppendZeroIfFloat(setting.SettingType); + var result = GUILayout.TextField(text, ConfigurationManager.buttonStyle, GUILayout.MaxWidth(rightColumnWidth)); + if (result != text) + setting.Set(setting.StrToObj(result)); + } + else + { + // Fall back to slow/less reliable method + var rawValue = setting.Get(); + var value = rawValue == null ? "NULL" : rawValue.ToString().AppendZeroIfFloat(setting.SettingType); + if (CanCovert(value, setting.SettingType)) + { + var result = GUILayout.TextField(value, ConfigurationManager.buttonStyle, GUILayout.MaxWidth(rightColumnWidth)); + if (result != value) + setting.Set(Convert.ChangeType(result, setting.SettingType, CultureInfo.InvariantCulture)); + } + else + { + GUILayout.TextArea(value, ConfigurationManager.buttonStyle, GUILayout.MaxWidth(rightColumnWidth)); + } + } + + // When using MaxWidth the width will always be less than full window size, use this to fill this gap and push the Reset button to the right edge + GUILayout.FlexibleSpace(); + } + + private readonly Dictionary _canCovertCache = new Dictionary(); + private bool CanCovert(string value, Type type) + { + if (_canCovertCache.ContainsKey(type)) + return _canCovertCache[type]; + + try + { + var _ = Convert.ChangeType(value, type); + _canCovertCache[type] = true; + return true; + } + catch + { + _canCovertCache[type] = false; + return false; + } + } + + private static void DrawKeyboardShortcut(SettingEntryBase setting) + { +#pragma warning disable 618 // Disable obsolete warning + var value = setting.Get(); + + if (_currentKeyboardShortcutToSet == setting) + { + GUILayout.Label("Press any key combination", ConfigurationManager.labelStyle, GUILayout.ExpandWidth(true)); + GUIUtility.keyboardControl = -1; + + foreach (var key in _keysToCheck) + { + if (Input.GetKeyUp(key)) + { + setting.Set(new BepInEx.Configuration.KeyboardShortcut(key, _keysToCheck.Where(Input.GetKey).ToArray())); + _currentKeyboardShortcutToSet = null; + break; + } + } + + if (GUILayout.Button("Cancel", ConfigurationManager.buttonStyle, GUILayout.ExpandWidth(false))) + _currentKeyboardShortcutToSet = null; + } + else + { + if (GUILayout.Button(value.ToString(), GUILayout.ExpandWidth(true))) + _currentKeyboardShortcutToSet = setting; + + if (GUILayout.Button("Clear", ConfigurationManager.buttonStyle, GUILayout.ExpandWidth(false))) + { + setting.Set(BepInEx.Configuration.KeyboardShortcut.Empty); + _currentKeyboardShortcutToSet = null; + } + } +#pragma warning restore 618 + } + + private static void DrawVector2(SettingEntryBase obj) + { + var setting = (Vector2)obj.Get(); + var copy = setting; + setting.x = DrawSingleVectorSlider(setting.x, "X"); + setting.y = DrawSingleVectorSlider(setting.y, "Y"); + if (setting != copy) obj.Set(setting); + } + + private static void DrawVector3(SettingEntryBase obj) + { + var setting = (Vector3)obj.Get(); + var copy = setting; + setting.x = DrawSingleVectorSlider(setting.x, "X"); + setting.y = DrawSingleVectorSlider(setting.y, "Y"); + setting.z = DrawSingleVectorSlider(setting.z, "Z"); + if (setting != copy) obj.Set(setting); + } + + private static void DrawVector4(SettingEntryBase obj) + { + var setting = (Vector4)obj.Get(); + var copy = setting; + setting.x = DrawSingleVectorSlider(setting.x, "X"); + setting.y = DrawSingleVectorSlider(setting.y, "Y"); + setting.z = DrawSingleVectorSlider(setting.z, "Z"); + setting.w = DrawSingleVectorSlider(setting.w, "W"); + if (setting != copy) obj.Set(setting); + } + + private static void DrawQuaternion(SettingEntryBase obj) + { + var setting = (Quaternion)obj.Get(); + var copy = setting; + setting.x = DrawSingleVectorSlider(setting.x, "X"); + setting.y = DrawSingleVectorSlider(setting.y, "Y"); + setting.z = DrawSingleVectorSlider(setting.z, "Z"); + setting.w = DrawSingleVectorSlider(setting.w, "W"); + if (setting != copy) obj.Set(setting); + } + + private static float DrawSingleVectorSlider(float setting, string label) + { + GUILayout.Label(label, ConfigurationManager.labelStyle, GUILayout.ExpandWidth(false)); + float.TryParse(GUILayout.TextField(setting.ToString("F", CultureInfo.InvariantCulture), GUILayout.ExpandWidth(true)), NumberStyles.Any, CultureInfo.InvariantCulture, out var x); + return x; + } + + private static void DrawColor(SettingEntryBase obj) + { + var setting = (Color)obj.Get(); + + if (!_colorCache.TryGetValue(obj, out var cacheEntry)) + { + cacheEntry = new ColorCacheEntry { Tex = new Texture2D(40, 10, TextureFormat.ARGB32, false), Last = setting }; + cacheEntry.Tex.FillTexture(setting); + _colorCache[obj] = cacheEntry; + } + + GUILayout.Label("R", ConfigurationManager.labelStyle, GUILayout.ExpandWidth(false)); + setting.r = GUILayout.HorizontalSlider(setting.r, 0f, 1f, GUILayout.ExpandWidth(true)); + GUILayout.Label("G", ConfigurationManager.labelStyle, GUILayout.ExpandWidth(false)); + setting.g = GUILayout.HorizontalSlider(setting.g, 0f, 1f, GUILayout.ExpandWidth(true)); + GUILayout.Label("B", ConfigurationManager.labelStyle, GUILayout.ExpandWidth(false)); + setting.b = GUILayout.HorizontalSlider(setting.b, 0f, 1f, GUILayout.ExpandWidth(true)); + GUILayout.Label("A", ConfigurationManager.labelStyle, GUILayout.ExpandWidth(false)); + setting.a = GUILayout.HorizontalSlider(setting.a, 0f, 1f, GUILayout.ExpandWidth(true)); + + GUILayout.Space(4); + + if (setting != cacheEntry.Last) + { + obj.Set(setting); + cacheEntry.Tex.FillTexture(setting); + cacheEntry.Last = setting; + } + + GUILayout.Label(cacheEntry.Tex, ConfigurationManager.labelStyle, GUILayout.ExpandWidth(false)); + } + + private sealed class ColorCacheEntry + { + public Color Last; + public Texture2D Tex; + } + } +} \ No newline at end of file diff --git a/ConfigurationManager/SettingSearcher.cs b/ConfigurationManager/SettingSearcher.cs new file mode 100644 index 0000000..8d45db3 --- /dev/null +++ b/ConfigurationManager/SettingSearcher.cs @@ -0,0 +1,103 @@ +// Based on code made by MarC0 / ManlyMarco +// Copyright 2018 GNU General Public License v3.0 + +using System; +using BepInEx; +using BepInEx.Configuration; +using ConfigurationManager.Utilities; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; + +namespace ConfigurationManager +{ + internal static class SettingSearcher + { + private static readonly ICollection _updateMethodNames = new[] + { + "Update", + "FixedUpdate", + "LateUpdate", + "OnGUI" + }; + + public static void CollectSettings(out IEnumerable results, out List modsWithoutSettings, bool showDebug) + { + modsWithoutSettings = new List(); + + try + { + results = GetBepInExCoreConfig(); + } + catch (Exception ex) + { + results = Enumerable.Empty(); + ConfigurationManager.Logger.LogError(ex); + } + + foreach (var plugin in Utils.FindPlugins()) + { + var type = plugin.GetType(); + + var pluginInfo = plugin.Info.Metadata; + + if (type.GetCustomAttributes(typeof(BrowsableAttribute), false).Cast() + .Any(x => !x.Browsable)) + { + modsWithoutSettings.Add(pluginInfo.Name); + continue; + } + + var detected = new List(); + + detected.AddRange(GetPluginConfig(plugin).Cast()); + + detected.RemoveAll(x => x.Browsable == false); + + if (!detected.Any()) + modsWithoutSettings.Add(pluginInfo.Name); + + // Allow to enable/disable plugin if it uses any update methods ------ + if (showDebug && type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Any(x => _updateMethodNames.Contains(x.Name))) + { + // todo make a different class for it and fix access modifiers? + var enabledSetting = LegacySettingEntry.FromNormalProperty(plugin, type.GetProperty("enabled"), pluginInfo, plugin); + enabledSetting.DispName = "!Allow plugin to run on every frame"; + enabledSetting.Description = "Disabling this will disable some or all of the plugin's functionality.\nHooks and event-based functionality will not be disabled.\nThis setting will be lost after game restart."; + enabledSetting.IsAdvanced = true; + detected.Add(enabledSetting); + } + + if (detected.Any()) + { + results = results.Concat(detected); + } + } + } + + /// + /// Bepinex 5 config + /// + private static IEnumerable GetBepInExCoreConfig() + { + var coreConfigProp = typeof(ConfigFile).GetProperty("CoreConfig", BindingFlags.Static | BindingFlags.NonPublic); + if (coreConfigProp == null) throw new ArgumentNullException(nameof(coreConfigProp)); + + var coreConfig = (ConfigFile)coreConfigProp.GetValue(null, null); + var bepinMeta = new BepInPlugin("BepInEx", "BepInEx", typeof(BepInEx.Bootstrap.Chainloader).Assembly.GetName().Version.ToString()); + + return coreConfig.GetConfigEntries() + .Select(x => new ConfigSettingEntry(x, null) { IsAdvanced = true, PluginInfo = bepinMeta }) + .Cast(); + } + + /// + /// Used by bepinex 5 plugins + /// + private static IEnumerable GetPluginConfig(BaseUnityPlugin plugin) + { + return plugin.Config.GetConfigEntries().Select(x => new ConfigSettingEntry(x, plugin)); + } + } +} \ No newline at end of file diff --git a/ConfigurationManager/Utilities/ComboBox.cs b/ConfigurationManager/Utilities/ComboBox.cs new file mode 100644 index 0000000..8acdf70 --- /dev/null +++ b/ConfigurationManager/Utilities/ComboBox.cs @@ -0,0 +1,138 @@ +// Popup list created by Eric Haines +// ComboBox Extended by Hyungseok Seo.(Jerry) sdragoon@nate.com +// this oop version of ComboBox is refactored by zhujiangbo jumbozhu@gmail.com +// Based on code made by MarC0 / ManlyMarco +// Copyright 2018 GNU General Public License v3.0 + + +using System; +using UnityEngine; + +namespace ConfigurationManager.Utilities +{ + internal class ComboBox + { + private static bool forceToUnShow; + private static int useControlID = -1; + private readonly string boxStyle; + private readonly string buttonStyle; + private bool isClickedComboButton; + private readonly GUIContent[] listContent; + private readonly GUIStyle listStyle; + private readonly int _windowYmax; + + public ComboBox(Rect rect, GUIContent buttonContent, GUIContent[] listContent, GUIStyle listStyle, float windowYmax) + { + Rect = rect; + ButtonContent = buttonContent; + this.listContent = listContent; + buttonStyle = "button"; + boxStyle = "box"; + this.listStyle = listStyle; + _windowYmax = (int)windowYmax; + } + + public ComboBox(Rect rect, GUIContent buttonContent, GUIContent[] listContent, string buttonStyle, + string boxStyle, GUIStyle listStyle) + { + Rect = rect; + ButtonContent = buttonContent; + this.listContent = listContent; + this.buttonStyle = buttonStyle; + this.boxStyle = boxStyle; + this.listStyle = listStyle; + } + + public Rect Rect { get; set; } + + public GUIContent ButtonContent { get; set; } + + public void Show(Action onItemSelected) + { + if (forceToUnShow) + { + forceToUnShow = false; + isClickedComboButton = false; + } + + var done = false; + var controlID = GUIUtility.GetControlID(FocusType.Passive); + + Vector2 currentMousePosition = Vector2.zero; + if (Event.current.GetTypeForControl(controlID) == EventType.MouseUp) + { + if (isClickedComboButton) + { + done = true; + currentMousePosition = Event.current.mousePosition; + } + } + + if (GUI.Button(Rect, ButtonContent, buttonStyle)) + { + if (useControlID == -1) + { + useControlID = controlID; + isClickedComboButton = false; + } + + if (useControlID != controlID) + { + forceToUnShow = true; + useControlID = controlID; + } + isClickedComboButton = true; + } + + if (isClickedComboButton) + { + GUI.enabled = false; + GUI.color = new Color(1, 1, 1, 2); + + var location = GUIUtility.GUIToScreenPoint(new Vector2(Rect.x, Rect.y + listStyle.CalcHeight(listContent[0], 1.0f))); + var size = new Vector2(Rect.width, listStyle.CalcHeight(listContent[0], 1.0f) * listContent.Length); + + var innerRect = new Rect(0, 0, size.x, size.y); + + var outerRectScreen = new Rect(location.x, location.y, size.x, size.y); + if (outerRectScreen.yMax > _windowYmax) + { + outerRectScreen.height = _windowYmax - outerRectScreen.y; + outerRectScreen.width += 20; + } + + if (currentMousePosition != Vector2.zero && outerRectScreen.Contains(GUIUtility.GUIToScreenPoint(currentMousePosition))) + done = false; + + CurrentDropdownDrawer = () => + { + GUI.enabled = true; + + var scrpos = GUIUtility.ScreenToGUIPoint(location); + var outerRectLocal = new Rect(scrpos.x, scrpos.y, outerRectScreen.width, outerRectScreen.height); + + GUI.Box(outerRectLocal, GUIContent.none, + new GUIStyle { normal = new GUIStyleState { background = ConfigurationManager.WindowBackground } }); + + _scrollPosition = GUI.BeginScrollView(outerRectLocal, _scrollPosition, innerRect, false, false); + { + const int initialSelectedItem = -1; + var newSelectedItemIndex = GUI.SelectionGrid(innerRect, initialSelectedItem, listContent, 1, listStyle); + if (newSelectedItemIndex != initialSelectedItem) + { + onItemSelected(newSelectedItemIndex); + isClickedComboButton = false; + } + } + GUI.EndScrollView(true); + }; + } + + if (done) + isClickedComboButton = false; + } + + private Vector2 _scrollPosition = Vector2.zero; + public static Action CurrentDropdownDrawer { get; set; } + } +} \ No newline at end of file diff --git a/ConfigurationManager/Utilities/Utilities.cs b/ConfigurationManager/Utilities/Utilities.cs new file mode 100644 index 0000000..f12cc0e --- /dev/null +++ b/ConfigurationManager/Utilities/Utilities.cs @@ -0,0 +1,146 @@ +// Based on code made by MarC0 / ManlyMarco +// Copyright 2018 GNU General Public License v3.0 + +using BepInEx; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace ConfigurationManager.Utilities +{ + internal static class Utils + { + public static string ToProperCase(this string str) + { + if (string.IsNullOrEmpty(str)) return string.Empty; + if (str.Length < 2) return str; + + // Start with the first character. + string result = str.Substring(0, 1).ToUpper(); + + // Add the remaining characters. + for (int i = 1; i < str.Length; i++) + { + if (char.IsUpper(str[i])) result += " "; + result += str[i]; + } + + return result; + } + + /// + /// Return items with browsable attribute same as expectedBrowsable, and optionally items with no browsable attribute + /// + public static IEnumerable FilterBrowsable(this IEnumerable props, bool expectedBrowsable, + bool includeNotSet = false) where T : MemberInfo + { + if (includeNotSet) + return props.Where(p => p.GetCustomAttributes(typeof(BrowsableAttribute), false).Cast().All(x => x.Browsable == expectedBrowsable)); + + return props.Where(p => p.GetCustomAttributes(typeof(BrowsableAttribute), false).Cast().Any(x => x.Browsable == expectedBrowsable)); + } + + public static bool IsSubclassOfRawGeneric(this Type toCheck, Type generic) + { + while (toCheck != null && toCheck != typeof(object)) + { + var cur = toCheck.IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck; + if (generic == cur) + return true; + toCheck = toCheck.BaseType; + } + return false; + } + + // Search for objects instead of using chainloader API to find dynamically loaded plugins + public static BaseUnityPlugin[] FindPlugins() => Object.FindObjectsOfType(typeof(BaseUnityPlugin)).Cast().ToArray(); + + public static bool IsNumber(this object value) => value is sbyte + || value is byte + || value is short + || value is ushort + || value is int + || value is uint + || value is long + || value is ulong + || value is float + || value is double + || value is decimal; + + public static string AppendZero(this string s) + { + return !s.Contains(".") ? s + ".0" : s; + } + + public static string AppendZeroIfFloat(this string s, Type type) + { + return type == typeof(float) || type == typeof(double) || type == typeof(decimal) ? s.AppendZero() : s; + } + + public static void FillTexture(this Texture2D tex, Color color) + { + for (var x = 0; x < tex.width; x++) + for (var y = 0; y < tex.height; y++) + tex.SetPixel(x, y, color); + + tex.Apply(false); + } + + public static void OpenLog() + { + bool TryOpen(string path) + { + if (!File.Exists(path)) return false; + try + { + Process.Start(path); + return true; + } + catch + { + return false; + } + } + + // Redirected by preloader to game root + if (TryOpen(Path.Combine(Path.Combine(Application.dataPath, ".."), "output_log.txt"))) return; + + // Generated in most versions unless disabled + if (TryOpen(Path.Combine(Application.dataPath, "output_log.txt"))) return; + + // Available since 2018.3 + var prop = typeof(Application).GetProperty("consoleLogPath", BindingFlags.Static | BindingFlags.Public); + if (prop != null) + { + var path = prop.GetValue(null, null) as string; + if (TryOpen(path)) return; + } + + if (Directory.Exists(Application.persistentDataPath)) + { + var file = Directory.GetFiles(Application.persistentDataPath, "output_log.txt", SearchOption.AllDirectories).FirstOrDefault(); + if (TryOpen(file)) return; + } + + // Fall back to more aggresive brute search + var rootDir = Directory.GetParent(Application.dataPath); + if (rootDir.Exists) + { + // BepInEx 5.x log file + var result = rootDir.GetFiles("LogOutput.log", SearchOption.AllDirectories).FirstOrDefault(); + if (result == null) + result = rootDir.GetFiles("output_log.txt", SearchOption.AllDirectories).FirstOrDefault(); + + if (result != null && TryOpen(result.FullName)) return; + } + + throw new FileNotFoundException("No log files were found"); + } + } +} diff --git a/ConfigurationManager/ValueChangedEventArgs.cs b/ConfigurationManager/ValueChangedEventArgs.cs new file mode 100644 index 0000000..620e5dd --- /dev/null +++ b/ConfigurationManager/ValueChangedEventArgs.cs @@ -0,0 +1,24 @@ +// Based on code made by MarC0 / ManlyMarco +// Copyright 2018 GNU General Public License v3.0 + + +using System; + +namespace ConfigurationManager +{ + /// + /// Arguments representing a change in value + /// + public sealed class ValueChangedEventArgs : EventArgs + { + /// + public ValueChangedEventArgs(TValue newValue) + { + NewValue = newValue; + } + /// + /// Newly assigned value + /// + public TValue NewValue { get; } + } +} diff --git a/ContainersAnywhere/AedenthornUtils.cs b/ContainersAnywhere/AedenthornUtils.cs index 5be8404..f8b100f 100644 --- a/ContainersAnywhere/AedenthornUtils.cs +++ b/ContainersAnywhere/AedenthornUtils.cs @@ -6,7 +6,7 @@ public static bool IgnoreKeyPresses(bool extra = false) { if (!extra) return ZNetScene.instance == null || Player.m_localPlayer == null || Minimap.IsOpen() || Console.IsVisible() || TextInput.IsVisible() || ZNet.instance.InPasswordDialog() || Chat.instance?.HasFocus() == true; - return ZNetScene.instance == null || Player.m_localPlayer == null || Minimap.IsOpen() || Console.IsVisible() || TextInput.IsVisible() || ZNet.instance.InPasswordDialog() || Chat.instance?.HasFocus() == true || StoreGui.IsVisible() || InventoryGui.IsVisible() || Menu.IsVisible() || TextViewer.instance || TextViewer.instance.IsVisible(); + return ZNetScene.instance == null || Player.m_localPlayer == null || Minimap.IsOpen() || Console.IsVisible() || TextInput.IsVisible() || ZNet.instance.InPasswordDialog() || Chat.instance?.HasFocus() == true || StoreGui.IsVisible() || InventoryGui.IsVisible() || Menu.IsVisible() || TextViewer.instance?.IsVisible() == true; } public static bool CheckKeyDown(string value) { @@ -19,4 +19,15 @@ public static bool CheckKeyDown(string value) return false; } } + public static bool CheckKeyHeld(string value, bool req = true) + { + try + { + return Input.GetKey(value.ToLower()); + } + catch + { + return !req; + } + } } diff --git a/CustomAudio/CustomAudio.csproj b/CustomAudio/CustomAudio.csproj index c9aee03..7f0eb6e 100644 --- a/CustomAudio/CustomAudio.csproj +++ b/CustomAudio/CustomAudio.csproj @@ -115,6 +115,7 @@ - call $(SolutionDir)copyDll.bat $(ProjectName) + + \ No newline at end of file diff --git a/CustomToolbarHotkeys/AedenthornUtils.cs b/CustomToolbarHotkeys/AedenthornUtils.cs new file mode 100644 index 0000000..f8b100f --- /dev/null +++ b/CustomToolbarHotkeys/AedenthornUtils.cs @@ -0,0 +1,33 @@ +using UnityEngine; + +public class AedenthornUtils +{ + public static bool IgnoreKeyPresses(bool extra = false) + { + if (!extra) + return ZNetScene.instance == null || Player.m_localPlayer == null || Minimap.IsOpen() || Console.IsVisible() || TextInput.IsVisible() || ZNet.instance.InPasswordDialog() || Chat.instance?.HasFocus() == true; + return ZNetScene.instance == null || Player.m_localPlayer == null || Minimap.IsOpen() || Console.IsVisible() || TextInput.IsVisible() || ZNet.instance.InPasswordDialog() || Chat.instance?.HasFocus() == true || StoreGui.IsVisible() || InventoryGui.IsVisible() || Menu.IsVisible() || TextViewer.instance?.IsVisible() == true; + } + public static bool CheckKeyDown(string value) + { + try + { + return Input.GetKeyDown(value.ToLower()); + } + catch + { + return false; + } + } + public static bool CheckKeyHeld(string value, bool req = true) + { + try + { + return Input.GetKey(value.ToLower()); + } + catch + { + return !req; + } + } +} diff --git a/CustomToolbarHotkeys/BepInExPlugin.cs b/CustomToolbarHotkeys/BepInExPlugin.cs index bc62211..3d19f3c 100644 --- a/CustomToolbarHotkeys/BepInExPlugin.cs +++ b/CustomToolbarHotkeys/BepInExPlugin.cs @@ -9,7 +9,7 @@ namespace CustomToolbarHotkeys { - [BepInPlugin("aedenthorn.CustomToolbarHotkeys", "Custom Toolbar Hotkeys", "0.2.0")] + [BepInPlugin("aedenthorn.CustomToolbarHotkeys", "Custom Toolbar Hotkeys", "0.2.2")] public class BepInExPlugin : BaseUnityPlugin { private static readonly bool isDebug = true; @@ -95,24 +95,24 @@ static class Player_Update_Patch { static bool Prefix(Player __instance) { - if (!modEnabled.Value) + if (!modEnabled.Value || AedenthornUtils.IgnoreKeyPresses()) return true; - if (Input.GetKeyDown(hotKey1.Value)) + if (AedenthornUtils.CheckKeyDown(hotKey1.Value)) __instance.UseHotbarItem(1); - else if (Input.GetKeyDown(hotKey2.Value)) + else if (AedenthornUtils.CheckKeyDown(hotKey2.Value)) __instance.UseHotbarItem(2); - else if (Input.GetKeyDown(hotKey3.Value)) + else if (AedenthornUtils.CheckKeyDown(hotKey3.Value)) __instance.UseHotbarItem(3); - else if (Input.GetKeyDown(hotKey4.Value)) + else if (AedenthornUtils.CheckKeyDown(hotKey4.Value)) __instance.UseHotbarItem(4); - else if (Input.GetKeyDown(hotKey5.Value)) + else if (AedenthornUtils.CheckKeyDown(hotKey5.Value)) __instance.UseHotbarItem(5); - else if (Input.GetKeyDown(hotKey6.Value)) + else if (AedenthornUtils.CheckKeyDown(hotKey6.Value)) __instance.UseHotbarItem(6); - else if (Input.GetKeyDown(hotKey7.Value)) + else if (AedenthornUtils.CheckKeyDown(hotKey7.Value)) __instance.UseHotbarItem(7); - else if (Input.GetKeyDown(hotKey8.Value)) + else if (AedenthornUtils.CheckKeyDown(hotKey8.Value)) __instance.UseHotbarItem(8); else return true; diff --git a/CustomToolbarHotkeys/CustomToolbarHotkeys.csproj b/CustomToolbarHotkeys/CustomToolbarHotkeys.csproj index 80a6d88..107dfdd 100644 --- a/CustomToolbarHotkeys/CustomToolbarHotkeys.csproj +++ b/CustomToolbarHotkeys/CustomToolbarHotkeys.csproj @@ -99,6 +99,7 @@ + diff --git a/SleepWithoutSpawn/AedenthornUtils.cs b/SleepWithoutSpawn/AedenthornUtils.cs index c1c018b..f8b100f 100644 --- a/SleepWithoutSpawn/AedenthornUtils.cs +++ b/SleepWithoutSpawn/AedenthornUtils.cs @@ -6,7 +6,7 @@ public static bool IgnoreKeyPresses(bool extra = false) { if (!extra) return ZNetScene.instance == null || Player.m_localPlayer == null || Minimap.IsOpen() || Console.IsVisible() || TextInput.IsVisible() || ZNet.instance.InPasswordDialog() || Chat.instance?.HasFocus() == true; - return ZNetScene.instance == null || Player.m_localPlayer == null || Minimap.IsOpen() || Console.IsVisible() || TextInput.IsVisible() || ZNet.instance.InPasswordDialog() || Chat.instance?.HasFocus() == true || StoreGui.IsVisible() || InventoryGui.IsVisible() || Menu.IsVisible() || TextViewer.instance || TextViewer.instance.IsVisible(); + return ZNetScene.instance == null || Player.m_localPlayer == null || Minimap.IsOpen() || Console.IsVisible() || TextInput.IsVisible() || ZNet.instance.InPasswordDialog() || Chat.instance?.HasFocus() == true || StoreGui.IsVisible() || InventoryGui.IsVisible() || Menu.IsVisible() || TextViewer.instance?.IsVisible() == true; } public static bool CheckKeyDown(string value) { @@ -19,7 +19,7 @@ public static bool CheckKeyDown(string value) return false; } } - public static bool CheckKeyHeld(string value) + public static bool CheckKeyHeld(string value, bool req = true) { try { @@ -27,7 +27,7 @@ public static bool CheckKeyHeld(string value) } catch { - return false; + return !req; } } } diff --git a/ValheimMods.sln b/ValheimMods.sln index 0109430..6179c5c 100644 --- a/ValheimMods.sln +++ b/ValheimMods.sln @@ -122,6 +122,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution LICENSE.txt = LICENSE.txt EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConfigurationManager", "ConfigurationManager\ConfigurationManager.csproj", "{104215E5-C277-4D43-A52C-B31467312299}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -356,6 +358,10 @@ Global {348B29C1-CBCE-41BA-A524-60B9FBC49757}.Debug|Any CPU.Build.0 = Debug|Any CPU {348B29C1-CBCE-41BA-A524-60B9FBC49757}.Release|Any CPU.ActiveCfg = Release|Any CPU {348B29C1-CBCE-41BA-A524-60B9FBC49757}.Release|Any CPU.Build.0 = Release|Any CPU + {104215E5-C277-4D43-A52C-B31467312299}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {104215E5-C277-4D43-A52C-B31467312299}.Debug|Any CPU.Build.0 = Debug|Any CPU + {104215E5-C277-4D43-A52C-B31467312299}.Release|Any CPU.ActiveCfg = Release|Any CPU + {104215E5-C277-4D43-A52C-B31467312299}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE