/* * Copyright 2022 Peter Han * Permission is hereby granted, free of charge, to any person obtaining a copy of this software * and associated documentation files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ using HarmonyLib; using PeterHan.PLib.Core; using PeterHan.PLib.Detours; using System; using System.Collections.Generic; using System.Reflection; namespace PeterHan.PLib.Actions { /// /// Manages PAction functionality which must be single instance. /// public sealed class PActionManager : PForwardedComponent { /// /// Prototypes the required parameters for new BindingEntry() since they changed in /// U39-489490. /// private delegate BindingEntry NewEntry(string group, GamepadButton button, KKeyCode key_code, Modifier modifier, Action action); /// /// The category used for all PLib keys. /// public const string CATEGORY = "PLib"; /// /// Creates a new BindingEntry. /// private static readonly NewEntry NEW_BINDING_ENTRY = typeof(BindingEntry). DetourConstructor(); /// /// The version of this component. Uses the running PLib version. /// internal static readonly Version VERSION = new Version(PVersion.VERSION); /// /// The instantiated copy of this class. /// internal static PActionManager Instance { get; private set; } /// /// Assigns the key bindings to each Action when they are needed. /// private static void AssignKeyBindings() { var allMods = PRegistry.Instance.GetAllComponents(typeof(PActionManager).FullName); if (allMods != null) foreach (var mod in allMods) mod.Process(0, null); } private static void CKeyDef_Postfix(KInputController.KeyDef __instance) { if (Instance != null) __instance.mActionFlags = ExtendFlags(__instance.mActionFlags, Instance.GetMaxAction()); } /// /// Extends the action flags array to the new maximum length. /// /// The old flags array. /// The minimum length. /// The new action flags array. internal static bool[] ExtendFlags(bool[] oldActionFlags, int newMax) { int n = oldActionFlags.Length; bool[] newActionFlags; if (n < newMax) { newActionFlags = new bool[newMax]; Array.Copy(oldActionFlags, newActionFlags, n); } else newActionFlags = oldActionFlags; return newActionFlags; } /// /// Checks to see if an action is already bound to a key. /// /// The current key bindings. /// The action to look up. /// true if the action already has a binding assigned, or false otherwise. private static bool FindKeyBinding(IEnumerable currentBindings, Action action) { bool inBindings = false; foreach (var entry in currentBindings) if (entry.mAction == action) { LogKeyBind("Action {0} already exists; assigned to KeyCode {1}".F(action, entry.mKeyCode)); inBindings = true; break; } return inBindings; } /// /// Retrieves a Klei key binding title. /// /// The category of the key binding. /// The key binding to retrieve. /// The Strings entry describing this key binding. private static string GetBindingTitle(string category, string item) { return "STRINGS.INPUT_BINDINGS." + category.ToUpperInvariant() + "." + item. ToUpperInvariant(); } /// /// Retrieves a "localized" (if PLib is localized) description of additional key codes /// from the KKeyCode enumeration, to avoid warning spam on popular keybinds like /// arrow keys, delete, home, and so forth. /// /// The key code. /// A description of that key code, or null if no localization is found. internal static string GetExtraKeycodeLocalized(KKeyCode code) { string localCode = null; switch (code) { case KKeyCode.Home: localCode = PLibStrings.KEY_HOME; break; case KKeyCode.End: localCode = PLibStrings.KEY_END; break; case KKeyCode.Delete: localCode = PLibStrings.KEY_DELETE; break; case KKeyCode.PageDown: localCode = PLibStrings.KEY_PAGEDOWN; break; case KKeyCode.PageUp: localCode = PLibStrings.KEY_PAGEUP; break; case KKeyCode.LeftArrow: localCode = PLibStrings.KEY_ARROWLEFT; break; case KKeyCode.UpArrow: localCode = PLibStrings.KEY_ARROWUP; break; case KKeyCode.RightArrow: localCode = PLibStrings.KEY_ARROWRIGHT; break; case KKeyCode.DownArrow: localCode = PLibStrings.KEY_ARROWDOWN; break; case KKeyCode.Pause: localCode = PLibStrings.KEY_PAUSE; break; case KKeyCode.SysReq: localCode = PLibStrings.KEY_SYSRQ; break; case KKeyCode.Print: localCode = PLibStrings.KEY_PRTSCREEN; break; default: break; } return localCode; } private static bool GetKeycodeLocalized_Prefix(KKeyCode key_code, ref string __result) { string newResult = GetExtraKeycodeLocalized(key_code); if (newResult != null) __result = newResult; return newResult == null; } private static void IsActive_Prefix(ref bool[] ___mActionState) { if (Instance != null) ___mActionState = ExtendFlags(___mActionState, Instance.GetMaxAction()); } /// /// Logs a message encountered by the PLib key binding system. /// /// The message. internal static void LogKeyBind(string message) { Debug.LogFormat("[PKeyBinding] {0}", message); } /// /// Logs a warning encountered by the PLib key binding system. /// /// The warning message. internal static void LogKeyBindWarning(string message) { Debug.LogWarningFormat("[PKeyBinding] {0}", message); } private static void QueueButtonEvent_Prefix(ref bool[] ___mActionState, KInputController.KeyDef key_def) { if (KInputManager.isFocused && Instance != null) { int max = Instance.GetMaxAction(); key_def.mActionFlags = ExtendFlags(key_def.mActionFlags, max); ___mActionState = ExtendFlags(___mActionState, max); } } private static void SetDefaultKeyBindings_Postfix() { Instance?.UpdateMaxAction(); } public override Version Version => VERSION; /// /// Queued key binds which are resolved on load. /// private readonly IList actions; /// /// The maximum action index of any custom action registered across all mods. /// private int maxAction; /// /// Creates a new action manager used to create and assign custom actions. Due to the /// timing of when the user's key bindings are loaded, all actions must be added in /// OnLoad(). /// public PActionManager() { actions = new List(8); maxAction = 0; } public override void Bootstrap(Harmony plibInstance) { // GameInputMapping.LoadBindings occurs after mods load but before the rest of the // PLib components are initialized plibInstance.Patch(typeof(GameInputMapping), nameof(GameInputMapping. LoadBindings), prefix: PatchMethod(nameof(AssignKeyBindings))); } /// /// Registers a PAction with the action manager. /// /// This call should occur after PUtil.InitLibrary() during the mod OnLoad(). If called /// earlier, it may fail with InvalidOperationException, and if called later, the /// user's custom key bind (if applicable) will be discarded. /// /// The identifier for this action. /// The action's title. If null, the default value from /// STRINGS.INPUT_BINDINGS.PLIB.identifier will be used instead. /// The default key binding for this action. If null, no key will /// be bound by default, but the user can set a key bind. /// The action thus registered. public PAction CreateAction(string identifier, LocString title, PKeyBinding binding = null) { PAction action; RegisterForForwarding(); int curIndex = GetSharedData(1); action = new PAction(curIndex, identifier, title, binding); SetSharedData(curIndex + 1); actions.Add(action); return action; } /// /// Returns the maximum length of the Action enum, including custom actions. If no /// actions are defined, returns NumActions - 1 since NumActions is reserved in the /// base game. /// /// This value will not be accurate until all mods have loaded and key binds /// registered (AfterLayerableLoad or later such as BeforeDbInit). /// /// The maximum length required to represent all Actions. public int GetMaxAction() { int nActions = (int)PAction.MaxAction; return (maxAction > 0) ? (maxAction + nActions) : nActions - 1; } public override void Initialize(Harmony plibInstance) { Instance = this; // GameInputMapping plibInstance.Patch(typeof(GameInputMapping), nameof(GameInputMapping. SetDefaultKeyBindings), postfix: PatchMethod(nameof( SetDefaultKeyBindings_Postfix))); // GameUtil plibInstance.Patch(typeof(GameUtil), nameof(GameUtil.GetKeycodeLocalized), prefix: PatchMethod(nameof(GetKeycodeLocalized_Prefix))); // KInputController plibInstance.PatchConstructor(typeof(KInputController.KeyDef), new Type[] { typeof(KKeyCode), typeof(Modifier) }, postfix: PatchMethod(nameof(CKeyDef_Postfix))); plibInstance.Patch(typeof(KInputController), nameof(KInputController.IsActive), prefix: PatchMethod(nameof(IsActive_Prefix))); plibInstance.Patch(typeof(KInputController), nameof(KInputController. QueueButtonEvent), prefix: PatchMethod(nameof(QueueButtonEvent_Prefix))); } public override void PostInitialize(Harmony plibInstance) { // Needs to occur after localization Strings.Add(GetBindingTitle(CATEGORY, "NAME"), PLibStrings.KEY_CATEGORY_TITLE); var allMods = PRegistry.Instance.GetAllComponents(ID); if (allMods != null) foreach (var mod in allMods) mod.Process(1, null); } public override void Process(uint operation, object _) { int n = actions.Count; if (n > 0) { if (operation == 0) RegisterKeyBindings(); else if (operation == 1) { #if DEBUG LogKeyBind("Localizing titles for {0}".F(Assembly.GetExecutingAssembly(). GetNameSafe() ?? "?")); #endif foreach (var action in actions) Strings.Add(GetBindingTitle(CATEGORY, action.GetKAction().ToString()), action.Title); } } } private void RegisterKeyBindings() { int n = actions.Count; LogKeyBind("Registering {0:D} key bind(s) for mod {1}".F(n, Assembly. GetExecutingAssembly().GetNameSafe() ?? "?")); var currentBindings = new List(GameInputMapping.DefaultBindings); foreach (var action in actions) { var kAction = action.GetKAction(); var binding = action.DefaultBinding; if (!FindKeyBinding(currentBindings, kAction)) { if (binding == null) binding = new PKeyBinding(); // This constructor changes often enough to be worth detouring currentBindings.Add(NEW_BINDING_ENTRY.Invoke(CATEGORY, binding. GamePadButton, binding.Key, binding.Modifiers, kAction)); } } GameInputMapping.SetDefaultKeyBindings(currentBindings.ToArray()); UpdateMaxAction(); } /// /// Updates the maximum action for this instance. /// private void UpdateMaxAction() { maxAction = GetSharedData(0); } } }