From 0adf4b53779a8070a7f6a08f0f0387aed2d1a238 Mon Sep 17 00:00:00 2001 From: Julian Kittel <62687014+ToniMacaroni@users.noreply.github.com> Date: Mon, 16 Oct 2023 18:22:14 +0200 Subject: [PATCH] keybind config --- CHANGELOG.md | 4 +- RedLoader/Preferences/ConfigCategory.cs | 45 +++++ RedLoader/Preferences/ConfigEntry.cs | 24 +++ RedLoader/Preferences/ConfigSystem.cs | 17 ++ RedLoader/Utils/Observable.cs | 15 ++ SonsGameManager/Core.cs | 11 +- SonsGameManager/ModSettingsUi.cs | 12 +- SonsSdk/CommonExtensions.cs | 21 +++ SonsSdk/ItemTools.cs | 92 ++++++++++ SonsSdk/ModInputCache.cs | 163 +++++++++++++++++ SonsSdk/SUI/PanelAnimator.cs | 80 ++++++++ SonsSdk/SUI/PanelBlur.cs | 63 +++++++ SonsSdk/SUI/SKeybindOptions.cs | 231 ++++++++++++++++++++++++ SonsSdk/SUI/SLabelOptions.cs | 12 -- SonsSdk/SUI/SUI.cs | 24 ++- SonsSdk/SUI/SUiElement.cs | 20 +- SonsSdk/SUI/SettingsRegistry.cs | 115 ++++++++---- SonsSdk/SdkEvents.cs | 2 + SonsSdk/SonsMod.cs | 4 + SonsSdk/SonsSdk.csproj | 18 ++ 20 files changed, 917 insertions(+), 56 deletions(-) create mode 100644 SonsSdk/ModInputCache.cs create mode 100644 SonsSdk/SUI/PanelAnimator.cs create mode 100644 SonsSdk/SUI/PanelBlur.cs create mode 100644 SonsSdk/SUI/SKeybindOptions.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index ec73f96..ab1f908 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ - Added `gotopickup` command to quickly go to a pickup location (useful for finding story items) - Added `dump` command to dump various data (like `dump items` for a list of all items) -- Fix settings getting combined when exiting via escape key \ No newline at end of file +- Fix settings getting combined when exiting via escape key +- Fix boot.txt not being able to execute custom registered commands +- Added easier control over generated settings screens \ No newline at end of file diff --git a/RedLoader/Preferences/ConfigCategory.cs b/RedLoader/Preferences/ConfigCategory.cs index ecf78e6..bf8f06a 100644 --- a/RedLoader/Preferences/ConfigCategory.cs +++ b/RedLoader/Preferences/ConfigCategory.cs @@ -70,6 +70,50 @@ public ConfigEntry CreateEntry(string identifier, T default_value, string return entry; } + + public KeybindConfigEntry CreateKeybindEntry(string identifier, string key_name, string display_name = null, + string description = null, bool is_hidden = false, bool dont_save_default = false, Preferences.ValueValidator validator = null, string oldIdentifier = null) + { + if (string.IsNullOrEmpty(identifier)) + throw new Exception("identifier is null or empty when calling CreateEntry"); + + if (display_name == null) + display_name = identifier; + + var entry = GetKeybindEntry(identifier); + if (entry != null) + throw new Exception($"Calling CreateEntry for { display_name } when it Already Exists"); + + // if (validator != null && !validator.IsValid(default_value)) + // throw new ArgumentException($"Default value '{default_value}' is invalid according to the provided ValueValidator!"); + + if (oldIdentifier != null) + { + if (HasEntry(oldIdentifier)) + throw new Exception($"Unable to rename '{oldIdentifier}' when it got already loaded"); + + RenameEntry(oldIdentifier, identifier); + } + + entry = new KeybindConfigEntry( + identifier, + display_name, + description, + is_hidden, + dont_save_default, + this, + key_name, + validator); + + Preferences.IO.File currentFile = File; + if (currentFile == null) + currentFile = ConfigSystem.DefaultFile; + currentFile.SetupEntryFromRawValue(entry); + + Entries.Add(entry); + + return entry; + } public bool DeleteEntry(string identifier) { @@ -106,6 +150,7 @@ public ConfigEntry GetEntry(string identifier) return Entries.Find(x => x.Identifier.Equals(identifier)); } public ConfigEntry GetEntry(string identifier) => (ConfigEntry)GetEntry(identifier); + public KeybindConfigEntry GetKeybindEntry(string identifier) => (KeybindConfigEntry)GetEntry(identifier); public bool HasEntry(string identifier) => GetEntry(identifier) != null; public void SetFilePath(string filepath) => SetFilePath(filepath, true, true); diff --git a/RedLoader/Preferences/ConfigEntry.cs b/RedLoader/Preferences/ConfigEntry.cs index ae700dd..1a6ddd1 100644 --- a/RedLoader/Preferences/ConfigEntry.cs +++ b/RedLoader/Preferences/ConfigEntry.cs @@ -184,4 +184,28 @@ internal void SetDefaultValue(T value) DontRegisterChanges = true; } } + + public class KeybindConfigEntry : ConfigEntry + { + public string KeyboardControlPath => "/" + Value; + + public KeybindConfigEntry( + string identifier, + string displayName, + string description, + bool isHidden, + bool dontSaveDefault, + ConfigCategory category, + string value, + ValueValidator validator) + : base(identifier, + displayName, + description, + isHidden, + dontSaveDefault, + category, + value, + validator) + { } + } } \ No newline at end of file diff --git a/RedLoader/Preferences/ConfigSystem.cs b/RedLoader/Preferences/ConfigSystem.cs index 16909bb..95c7bf5 100644 --- a/RedLoader/Preferences/ConfigSystem.cs +++ b/RedLoader/Preferences/ConfigSystem.cs @@ -229,6 +229,23 @@ public static ConfigEntry CreateEntry(string category_identifier, string e return category.CreateEntry(entry_identifier, default_value, display_name, description, is_hidden, dont_save_default, validator); } + + public static KeybindConfigEntry CreateKeybindEntry(string category_identifier, string entry_identifier, string key_name, + string display_name = null, string description = null, bool is_hidden = false, bool dont_save_default = false, + ValueValidator validator = null) + { + if (string.IsNullOrEmpty(category_identifier)) + throw new Exception("category_identifier is null or empty when calling CreateEntry"); + + if (string.IsNullOrEmpty(entry_identifier)) + throw new Exception("entry_identifier is null or empty when calling CreateEntry"); + + ConfigCategory category = GetCategory(entry_identifier); + if (category == null) + category = CreateCategory(category_identifier); + + return category.CreateKeybindEntry(entry_identifier, key_name, display_name, description, is_hidden, dont_save_default, validator); + } public static ConfigCategory GetCategory(string identifier) { diff --git a/RedLoader/Utils/Observable.cs b/RedLoader/Utils/Observable.cs index 18afdf1..99490e4 100644 --- a/RedLoader/Utils/Observable.cs +++ b/RedLoader/Utils/Observable.cs @@ -64,10 +64,25 @@ public void Set(T value) { Value = value; } + + /// + /// Removes all event listeners. + /// + public void RemoveEvents() + { + OnValueChanged = null; + } } public static class ObservableExtensions { + /// + /// Creates a new observable with the config value. + /// Also registers so that the config entry is updated when the observalble value changes. + /// + /// + /// + /// public static Observable ToObservable(this ConfigEntry configEntry) { var observable = new Observable(configEntry.Value); diff --git a/SonsGameManager/Core.cs b/SonsGameManager/Core.cs index 2f5786e..652d1c9 100644 --- a/SonsGameManager/Core.cs +++ b/SonsGameManager/Core.cs @@ -5,6 +5,7 @@ using Construction; using RedLoader; using RedLoader.Utils; +using Sons.Ai.Vail; using Sons.Gameplay; using Sons.Gui; using Sons.Items.Core; @@ -27,6 +28,7 @@ public Core() { Instance = this; OnUpdateCallback = OnUpdate; + OnCommandsRegisteredCallback = OnCommandsRegistered; } protected override void OnInitializeMod() @@ -103,13 +105,18 @@ protected override void OnGameStart() { RepositioningUtils.Manager.SetSkipPlaceAnimations(true); } - + + PanelBlur.SetupBlur(); + // -- Enable Bow Trajectory -- // if (Config.EnableBowTrajectory.Value) // { // BowTrajectory.Init(); // } - + } + + private void OnCommandsRegistered() + { LoadBootFile(); } diff --git a/SonsGameManager/ModSettingsUi.cs b/SonsGameManager/ModSettingsUi.cs index 16be3ce..cc4d907 100644 --- a/SonsGameManager/ModSettingsUi.cs +++ b/SonsGameManager/ModSettingsUi.cs @@ -1,4 +1,5 @@ using RedLoader; +using Sons.Ai.Vail; using SonsSdk; using SUI; using UnityEngine; @@ -15,7 +16,7 @@ public class ModSettingsUi private static readonly BackgroundDefinition MainBg = new( "#fff", GetBackgroundSprite(EBackground.Sons), - Image.Type.Sliced); + Image.Type.Simple); private static readonly BackgroundDefinition ButtonBg = new( ColorFromString("#191A23"), @@ -51,17 +52,18 @@ public static void Create() .Dock(EDockType.Top).Size(-100, 3).Position(0, -100)); _mainContainer = SScrollContainer.Margin(100,100,120,130).As(); + _mainContainer.ContainerObject.Padding(2, 4, 0, 0); panel.Add(_mainContainer); panel.Add(SBgButton .Background(ButtonBg).Background("#990508").Ppu(3) - .Pivot(1, 0).Anchor(AnchorType.BottomRight).Position(-40, 40).Size(300, 60) - .RichText("Back " + SpriteText("arrow_right")).FontSize(20).Notify(Close)); + .Pivot(0, 0).Anchor(AnchorType.BottomLeft).Position(80, 80).Size(300, 60) + .RichText(SpriteText("arrow_left") + " Back").FontSize(20).Notify(Close)); panel.Add(SBgButton .Background(ButtonBg).Background("#796C4E").Ppu(3) - .Pivot(0, 0).Anchor(AnchorType.BottomLeft).Position(40, 40).Size(300, 60) - .RichText(SpriteText("arrow_left") + " Revert").FontSize(20).Notify(RevertSettings)); + .Pivot(0, 0).Anchor(AnchorType.BottomLeft).Position(390, 80).Size(200, 60) + .RichText("Revert " + SpriteText("arrow_right")).FontSize(20).Notify(RevertSettings)); } public static void Open(string id) diff --git a/SonsSdk/CommonExtensions.cs b/SonsSdk/CommonExtensions.cs index 02005a3..0c21996 100644 --- a/SonsSdk/CommonExtensions.cs +++ b/SonsSdk/CommonExtensions.cs @@ -104,6 +104,14 @@ public static void Destroy(this GameObject go) where T : Component Object.Destroy(component); } } + + public static void TryDestroy(this GameObject go) + { + if (!go) + return; + + Object.Destroy(go); + } public static UnityEngine.Color ToUnityColor(this Color color) { @@ -151,4 +159,17 @@ public static T FirstContains(this IEnumerable iter, string name) where T { return iter.First(x => x.name.Contains(name)); } + + public static void Finish(this CancellationTokenSource cts) + { + cts.Cancel(); + cts.Dispose(); + } + + public static CancellationTokenSource ResetAndCreate(this CancellationTokenSource cts) + { + cts.Cancel(); + cts.Dispose(); + return new CancellationTokenSource(); + } } \ No newline at end of file diff --git a/SonsSdk/ItemTools.cs b/SonsSdk/ItemTools.cs index 55b7f41..ae71af3 100644 --- a/SonsSdk/ItemTools.cs +++ b/SonsSdk/ItemTools.cs @@ -2,6 +2,7 @@ using RedLoader.Utils; using Sons.Items.Core; using UnityEngine; +using UnityEngine.Localization.Settings; namespace SonsSdk; @@ -33,6 +34,97 @@ public static void AddItemComponent(int itemId, Type component, EPrefabType hook { ItemHookAdder.Add(new ItemHook(itemId, component, hookType)); } + + public static Transform GetHeldPrefab(int itemId) + { + var item = ItemDatabaseManager.ItemById(itemId); + if (!item) + return null; + + return item._heldPrefab; + } + + public static Transform GetPickupPrefab(int itemId) + { + var item = ItemDatabaseManager.ItemById(itemId); + if (!item) + return null; + + return item._pickupPrefab; + } + + public static Transform GetPropPrefab(int itemId) + { + var item = ItemDatabaseManager.ItemById(itemId); + if (!item) + return null; + + return item._propPrefab; + } + + public static (Texture icon, Texture outline) GetIcon(int itemId) + { + var item = ItemDatabaseManager.ItemById(itemId); + if (!item) + return default; + + return (item._uiData._icon, item._uiData._outlineIcon); + } + + public static void RegisterItem(ItemData itemData, bool autoTranslationEntry = true) + { + if (ItemDatabaseManager._itemsCache.ContainsKey(itemData._id)) + throw new Exception($"Item with id {itemData._id} already registered!"); + + ItemDatabaseManager._itemsCache.Add(itemData._id, itemData); + ItemDatabaseManager._instance._itemDataList.Add(itemData); + + if (itemData._uiData != null && autoTranslationEntry) + { + SetupLocalizationForItem(itemData._uiData, itemData._uiData._title); + } + } + + private static void SetupLocalizationForItem(ItemUiData itemUiData, string itemTitle) + { + var table = LocalizationSettings.StringDatabase.GetTable("Items"); + if (string.IsNullOrEmpty(itemUiData._translationKey)) + { + itemUiData._translationKey = $"I_{itemUiData._itemId}"; + } + + if (itemUiData._applyCustomActionText) + { + var leftActionText = itemUiData._leftActionCustomText; + var rightActionText = itemUiData._rightActionCustomText; + + if (!string.IsNullOrEmpty(leftActionText)) + { + var key = $"{itemUiData._translationKey}_LEFT_ACTION"; + table.AddEntry(key, leftActionText); + itemUiData._leftActionCustomText = key; + } + + if (!string.IsNullOrEmpty(rightActionText)) + { + var key = $"{itemUiData._translationKey}_RIGHT_ACTION"; + table.AddEntry(key, rightActionText); + itemUiData._rightActionCustomText = key; + } + } + + var description = itemUiData._description; + if (!string.IsNullOrEmpty(description)) + { + table.AddEntry($"{itemUiData._translationKey}_DESC", description); + } + + if (!string.IsNullOrEmpty(itemTitle)) + { + table.AddEntry(itemUiData._translationKey, itemTitle); + table.AddEntry($"{itemUiData._translationKey}_PLURAL", itemTitle + "s"); + } + } public struct ItemHook { diff --git a/SonsSdk/ModInputCache.cs b/SonsSdk/ModInputCache.cs new file mode 100644 index 0000000..246559d --- /dev/null +++ b/SonsSdk/ModInputCache.cs @@ -0,0 +1,163 @@ +using RedLoader; +using RedLoader.Preferences; +using UnityEngine.InputSystem; + +namespace SonsSdk; + +public static class ModInputCache +{ + internal static readonly Dictionary Keybinds = new(); + + public static ModKeybind GetKeybind(this KeybindConfigEntry config) + { + if (Keybinds.TryGetValue(config, out var action)) + { + return action; + } + + Keybinds[config] = action = new ModKeybind(config); + RLog.Msg("Registered keybind: " + config.Identifier + "::" + config.KeyboardControlPath); + action.Enable(); + return action; + } + + public static InputAction GetAction(this KeybindConfigEntry config) + { + return GetKeybind(config).Action; + } + + /// + /// Registers a callback for the "Performed" event of the input action. + /// + public static void Notify(this KeybindConfigEntry config, Action performAction = null, Action releasedAction = null) + { + var keybind = GetKeybind(config); + if (keybind == null) + { + return; + } + + keybind.Notify(performAction, releasedAction); + } + + /// + /// Removes a previously registered callback for the "Performed" event of the input action. + /// + public static void RemoveNotify(this KeybindConfigEntry config, Action performAction = null, Action releasedAction = null) + { + var keybind = GetKeybind(config); + if (keybind == null) + { + return; + } + + keybind.RemoveNotify(performAction, releasedAction); + } + + internal static void CheckAll() + { + foreach (var keybind in Keybinds.Values) + { + keybind.Check(); + } + } +} + +public class ModKeybind +{ + public readonly InputAction Action; + public readonly KeybindConfigEntry ConfigEntry; + + private event Action OnPerformed; + private event Action OnReleased; + + public ModKeybind(InputAction action, KeybindConfigEntry configEntry) + { + Action = action; + ConfigEntry = configEntry; + } + + public ModKeybind(KeybindConfigEntry configEntry) + { + ConfigEntry = configEntry; + + Action = new InputAction(configEntry.Identifier, binding: configEntry.KeyboardControlPath); + } + + /// + /// Registers a callback for the "Performed" event of the input action. + /// + /// + /// + public void Notify(Action performedAction = null, Action releasedAction = null) + { + if (performedAction != null) + { + OnPerformed += performedAction; + } + + if (releasedAction != null) + { + OnReleased += releasedAction; + } + } + + /// + /// Removes a previously registered callback for the "Performed" event of the input action. + /// + /// + public void RemoveNotify(Action performedAction = null, Action releasedAction = null) + { + if (performedAction != null) + { + OnPerformed -= performedAction; + } + + if (releasedAction != null) + { + OnReleased -= releasedAction; + } + } + + public void RemoveAllCallbacks() + { + OnPerformed = null; + } + + public void Enable() + { + Action.Enable(); + } + + public void Disable() + { + Action.Disable(); + } + + public void SaveToConfig() + { + if (Action.controls.Count == 0) + return; + + ConfigEntry.Value = Action.controls[0].name; + } + + public void RevertToDefault() + { + ConfigEntry.ResetToDefault(); + Action.ApplyBindingOverride(ConfigEntry.KeyboardControlPath); + } + + internal void Check() + { + if (Action.triggered) + { + OnPerformed?.Invoke(); + } + + if (Action.WasReleasedThisFrame()) + { + OnReleased?.Invoke(); + } + } +} \ No newline at end of file diff --git a/SonsSdk/SUI/PanelAnimator.cs b/SonsSdk/SUI/PanelAnimator.cs new file mode 100644 index 0000000..f969421 --- /dev/null +++ b/SonsSdk/SUI/PanelAnimator.cs @@ -0,0 +1,80 @@ +using Il2CppInterop.Runtime.Startup; +using Il2CppSystem; +using RedLoader; +using UnityEngine; +using Color = System.Drawing.Color; + +namespace SUI; + +public class PanelPosAnimator +{ + public Vector3? StartPos; + public Vector3 Distance; + public float Time; + + public PanelPosAnimator(Vector3 distance, float time = 0.5f) + { + Distance = distance; + Time = time; + } + + public PanelPosAnimator(float? x = null, float? y = null, float? z = null, float time = 0.5f) + { + Distance = new(x ?? 0, y ?? 0, z ?? 0); + Time = time; + } + + public void AnimIn(SContainerOptions container) + { + container.Active(true); + StartPos ??= container.RectTransform.localPosition; + container.RectTransform.localPosition = StartPos.Value - Distance; + + if (container.Root.GetComponent()) + return; + + iTween.MoveTo(container.Root, container.RectTransform.TransformPoint(Distance), Time); + } + + public void AnimOut(SContainerOptions container) + { + iTween.Stop(container.Root); + container.Active(false); + } + + public static PanelPosAnimator FromBottom = new(new Vector3(0, 300, 0)); + public static PanelPosAnimator FromTop = new(new Vector3(0, -300, 0)); + public static PanelPosAnimator FromLeft = new(new Vector3(300, 0, 0)); + public static PanelPosAnimator FromRight = new(new Vector3(-300, 0, 0)); +} + +public class PanelSizeAnimator +{ + public Vector2? StartSize; + public Vector2 Size; + public float Time; + + public PanelSizeAnimator(Vector2 size, float time = 0.5f) + { + Size = size; + Time = time; + } + + public void AnimIn(SContainerOptions container) + { + container.Active(true); + StartSize ??= container.RectTransform.sizeDelta; + container.RectTransform.sizeDelta = StartSize.Value - Size; + + if (container.Root.GetComponent()) + return; + + iTween.ScaleBy(container.Root, Size, Time); + } + + public void AnimOut(SContainerOptions container) + { + iTween.Stop(container.Root); + container.Active(false); + } +} \ No newline at end of file diff --git a/SonsSdk/SUI/PanelBlur.cs b/SonsSdk/SUI/PanelBlur.cs new file mode 100644 index 0000000..b7f4f3a --- /dev/null +++ b/SonsSdk/SUI/PanelBlur.cs @@ -0,0 +1,63 @@ +using RedLoader; +using TheForest.Utils; +using UnityEngine; +using UnityEngine.Rendering.HighDefinition; + +namespace SUI; + +public static class PanelBlur +{ + private static Dictionary _blurMaterials = new(); + internal static UIBlurPass CurrentBlurPass; + + public static Material GetForColor(Color color) + { + if (_blurMaterials.TryGetValue(color, out var blurMat)) + return blurMat; + + var newMat = new Material(Shader.Find("Sons/UI/SampleBlurHLSL")); + newMat.color = color; + _blurMaterials[color] = newMat; + return newMat; + } + + public static Material GetForShade(float shade) + { + return GetForColor(new(shade, shade, shade)); + } + + internal static void SetRadius(float radius) + { + if (CurrentBlurPass == null) + return; + + CurrentBlurPass.radius = radius; + } + + internal static void SetupBlur() + { + if (!LocalPlayer.Inventory) + return; + + var invBlur = LocalPlayer.Inventory.CameraController.Camera.transform.Find("SlightBlurPass"); + + if(!invBlur) + { + RLog.Error("Failed to find SlightBlurPass!"); + return; + } + + var go = UnityEngine.Object.Instantiate(invBlur.gameObject, LocalPlayer.MainCamTr, false); + + try + { + var volume = go.GetComponent(); + CurrentBlurPass = volume.customPasses._items[0].Cast(); + CurrentBlurPass.radius = 25; + } + catch (Exception e) + { + RLog.Error($"Failed to initialize UIBlurPass! {e}"); + } + } +} \ No newline at end of file diff --git a/SonsSdk/SUI/SKeybindOptions.cs b/SonsSdk/SUI/SKeybindOptions.cs new file mode 100644 index 0000000..9d758e3 --- /dev/null +++ b/SonsSdk/SUI/SKeybindOptions.cs @@ -0,0 +1,231 @@ +using System.Collections; +using Pathfinding.Util; +using RedLoader; +using Sons.Ai.Vail; +using Sons.Gui.Input; +using SonsSdk; +using TheForest.Utils; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.EventSystems; +using UnityEngine.InputSystem; +using UnityEngine.UI; +using Color = System.Drawing.Color; +using InputSystem = Sons.Input.InputSystem; +using Object = UnityEngine.Object; + +namespace SUI; + +public class SKeybindOptions : SUiElement +{ + internal static IconAssetDatabase IconAssetDatabase; + + public RebindingInputOptionGui RebindingInputOptionGui; + public DynamicInputIcon DynamicInputIcon; + public Button RebindButton; + + private EventSystem _eventSystem; + private InputActionRebindingExtensions.RebindingOperation _rebindingOperation; + private KeybindConfigEntry _keybindConfig; + private GameObject _instance; + + public SKeybindOptions(GameObject root) : base(root) + { + RebindingInputOptionGui = root.GetComponent(); + DynamicInputIcon = root.GetComponent(); + RebindButton = root.FindGet