Initial commit

This commit is contained in:
2022-12-30 00:58:33 +01:00
commit 3f1075e67f
176 changed files with 25715 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
/*
* 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 System;
namespace PeterHan.PLib.Actions {
/// <summary>
/// An Action managed by PLib. Actions have key bindings assigned to them.
/// </summary>
public sealed class PAction {
/// <summary>
/// The maximum action value (typically used to mean "no action") used in the currently
/// running instance of the game.
///
/// Since Action is compiled to a const int when a mod is built, any changes to the
/// Action enum will break direct references to Action.NumActions. Use this property
/// instead to always use the intended "no action" value.
/// </summary>
public static Action MaxAction { get; }
static PAction() {
if (!Enum.TryParse(nameof(Action.NumActions), out Action limit))
limit = Action.NumActions;
MaxAction = limit;
}
/// <summary>
/// The default key binding for this action. Not necessarily the current key binding.
/// </summary>
internal PKeyBinding DefaultBinding { get; }
/// <summary>
/// The action's non-localized identifier. Something like YOURMOD.CATEGORY.ACTIONNAME.
/// </summary>
public string Identifier { get; }
/// <summary>
/// The action's ID. This ID is assigned internally upon register and used for PLib
/// indexing. Even if you somehow obtain it in your mod, it is not to be used!
/// </summary>
private readonly int id;
/// <summary>
/// The action's title.
/// </summary>
public LocString Title { get; }
internal PAction(int id, string identifier, LocString title, PKeyBinding binding) {
if (id <= 0)
throw new ArgumentOutOfRangeException(nameof(id));
DefaultBinding = binding;
Identifier = identifier;
this.id = id;
Title = title;
}
public override bool Equals(object obj) {
return (obj is PAction other) && other.id == id;
}
/// <summary>
/// Retrieves the Klei action for this PAction.
/// </summary>
/// <returns>The Klei action for use in game functions.</returns>
public Action GetKAction() {
return (Action)((int)MaxAction + id);
}
public override int GetHashCode() {
return id;
}
public override string ToString() {
return "PAction[" + Identifier + "]: " + Title;
}
}
}

View File

@@ -0,0 +1,357 @@
/*
* 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 {
/// <summary>
/// Manages PAction functionality which must be single instance.
/// </summary>
public sealed class PActionManager : PForwardedComponent {
/// <summary>
/// Prototypes the required parameters for new BindingEntry() since they changed in
/// U39-489490.
/// </summary>
private delegate BindingEntry NewEntry(string group, GamepadButton button,
KKeyCode key_code, Modifier modifier, Action action);
/// <summary>
/// The category used for all PLib keys.
/// </summary>
public const string CATEGORY = "PLib";
/// <summary>
/// Creates a new BindingEntry.
/// </summary>
private static readonly NewEntry NEW_BINDING_ENTRY = typeof(BindingEntry).
DetourConstructor<NewEntry>();
/// <summary>
/// The version of this component. Uses the running PLib version.
/// </summary>
internal static readonly Version VERSION = new Version(PVersion.VERSION);
/// <summary>
/// The instantiated copy of this class.
/// </summary>
internal static PActionManager Instance { get; private set; }
/// <summary>
/// Assigns the key bindings to each Action when they are needed.
/// </summary>
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());
}
/// <summary>
/// Extends the action flags array to the new maximum length.
/// </summary>
/// <param name="oldActionFlags">The old flags array.</param>
/// <param name="newMax">The minimum length.</param>
/// <returns>The new action flags array.</returns>
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;
}
/// <summary>
/// Checks to see if an action is already bound to a key.
/// </summary>
/// <param name="currentBindings">The current key bindings.</param>
/// <param name="action">The action to look up.</param>
/// <returns>true if the action already has a binding assigned, or false otherwise.</returns>
private static bool FindKeyBinding(IEnumerable<BindingEntry> 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;
}
/// <summary>
/// Retrieves a Klei key binding title.
/// </summary>
/// <param name="category">The category of the key binding.</param>
/// <param name="item">The key binding to retrieve.</param>
/// <returns>The Strings entry describing this key binding.</returns>
private static string GetBindingTitle(string category, string item) {
return "STRINGS.INPUT_BINDINGS." + category.ToUpperInvariant() + "." + item.
ToUpperInvariant();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="code">The key code.</param>
/// <returns>A description of that key code, or null if no localization is found.</returns>
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());
}
/// <summary>
/// Logs a message encountered by the PLib key binding system.
/// </summary>
/// <param name="message">The message.</param>
internal static void LogKeyBind(string message) {
Debug.LogFormat("[PKeyBinding] {0}", message);
}
/// <summary>
/// Logs a warning encountered by the PLib key binding system.
/// </summary>
/// <param name="message">The warning message.</param>
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;
/// <summary>
/// Queued key binds which are resolved on load.
/// </summary>
private readonly IList<PAction> actions;
/// <summary>
/// The maximum action index of any custom action registered across all mods.
/// </summary>
private int maxAction;
/// <summary>
/// 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().
/// </summary>
public PActionManager() {
actions = new List<PAction>(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)));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="identifier">The identifier for this action.</param>
/// <param name="title">The action's title. If null, the default value from
/// STRINGS.INPUT_BINDINGS.PLIB.identifier will be used instead.</param>
/// <param name="binding">The default key binding for this action. If null, no key will
/// be bound by default, but the user can set a key bind.</param>
/// <returns>The action thus registered.</returns>
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;
}
/// <summary>
/// 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).
/// </summary>
/// <returns>The maximum length required to represent all Actions.</returns>
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<BindingEntry>(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();
}
/// <summary>
/// Updates the maximum action for this instance.
/// </summary>
private void UpdateMaxAction() {
maxAction = GetSharedData(0);
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* 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.
*/
namespace PeterHan.PLib.Actions {
/// <summary>
/// Represents a single key binding to an action.
/// </summary>
public sealed class PKeyBinding {
/// <summary>
/// The gamepad button to bind.
/// </summary>
public GamepadButton GamePadButton { get; set; }
/// <summary>
/// The key code.
/// </summary>
public KKeyCode Key { get; set; }
/// <summary>
/// The modifier code.
/// </summary>
public Modifier Modifiers { get; set; }
public PKeyBinding(KKeyCode keyCode = KKeyCode.None, Modifier modifiers = Modifier.
None, GamepadButton gamePadButton = GamepadButton.NumButtons) {
GamePadButton = gamePadButton;
Key = keyCode;
Modifiers = modifiers;
}
public PKeyBinding(PKeyBinding other) : this() {
if (other != null) {
GamePadButton = other.GamePadButton;
Key = other.Key;
Modifiers = other.Modifiers;
}
}
public override bool Equals(object obj) {
return obj is PKeyBinding other && other.Key == Key && other.Modifiers ==
Modifiers && other.GamePadButton == GamePadButton;
}
public override int GetHashCode() {
// Was auto generated by VS, please do not kill me
int hashCode = 1488379021 + GamePadButton.GetHashCode();
hashCode = hashCode * -1521134295 + Key.GetHashCode();
hashCode = hashCode * -1521134295 + Modifiers.GetHashCode();
return hashCode;
}
public override string ToString() {
return Modifiers + " " + Key;
}
}
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Title>PLib Actions</Title>
<AssemblyTitle>PLib.Actions</AssemblyTitle>
<Version>4.11.0.0</Version>
<UsesPLib>false</UsesPLib>
<RootNamespace>PeterHan.PLib.Actions</RootNamespace>
<AssemblyVersion>4.11.0.0</AssemblyVersion>
<DistributeMod>false</DistributeMod>
<PLibCore>true</PLibCore>
<Platforms>Vanilla;Mergedown</Platforms>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\$(Platform)\Release\PLibActions.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../PLibCore/PLibCore.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,118 @@
/*
* 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 PeterHan.PLib.Core;
using System;
using System.Collections.Generic;
namespace PeterHan.PLib.Actions {
/// <summary>
/// A tool mode used in custom tool menus. Shown in the options in the bottom right.
/// </summary>
public sealed class PToolMode {
/// <summary>
/// Sets up tool options in the tool parameter menu.
/// </summary>
/// <param name="menu">The menu to configure.</param>
/// <param name="options">The available modes.</param>
/// <returns>A dictionary which is updated in real time to contain the actual state of each mode.</returns>
public static IDictionary<string, ToolParameterMenu.ToggleState> PopulateMenu(
ToolParameterMenu menu, ICollection<PToolMode> options) {
if (options == null)
throw new ArgumentNullException(nameof(options));
var kOpt = new Dictionary<string, ToolParameterMenu.ToggleState>(options.Count);
// Add to Klei format, yes it loses the order but it means less of a mess
foreach (var option in options) {
string key = option.Key;
if (!string.IsNullOrEmpty(option.Title))
Strings.Add("STRINGS.UI.TOOLS.FILTERLAYERS." + key, option.Title);
kOpt.Add(key, option.State);
}
menu.PopulateMenu(kOpt);
return kOpt;
}
/// <summary>
/// Registers a tool with the game. It still must be added to a tool collection to be
/// visible.
/// </summary>
/// <typeparam name="T">The tool type to register.</typeparam>
/// <param name="controller">The player controller which will be its parent; consider
/// using in a postfix on PlayerController.OnPrefabInit.</param>
public static void RegisterTool<T>(PlayerController controller) where T : InterfaceTool
{
if (controller == null)
throw new ArgumentNullException(nameof(controller));
// Create list so that new tool can be appended at the end
var interfaceTools = ListPool<InterfaceTool, PlayerController>.Allocate();
interfaceTools.AddRange(controller.tools);
var newTool = new UnityEngine.GameObject(typeof(T).Name);
var tool = newTool.AddComponent<T>();
// Reparent tool to the player controller, then enable/disable to load it
newTool.transform.SetParent(controller.gameObject.transform);
newTool.gameObject.SetActive(true);
newTool.gameObject.SetActive(false);
interfaceTools.Add(tool);
controller.tools = interfaceTools.ToArray();
interfaceTools.Recycle();
}
/// <summary>
/// A unique key used to identify this mode.
/// </summary>
public string Key { get; }
/// <summary>
/// The current state of this tool mode.
/// </summary>
public ToolParameterMenu.ToggleState State { get; }
/// <summary>
/// The title displayed on-screen for this mode.
/// </summary>
public LocString Title { get; }
/// <summary>
/// Creates a new tool mode entry.
/// </summary>
/// <param name="key">The key which identifies this tool mode.</param>
/// <param name="title">The title to be displayed. If null, the title will be taken
/// from the default location in STRINGS.UI.TOOLS.FILTERLAYERS.</param>
/// <param name="state">The initial state, default Off.</param>
public PToolMode(string key, LocString title, ToolParameterMenu.ToggleState state =
ToolParameterMenu.ToggleState.Off) {
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException(nameof(key));
Key = key;
State = state;
Title = title;
}
public override bool Equals(object obj) {
return obj is PToolMode other && other.Key == Key;
}
public override int GetHashCode() {
return Key.GetHashCode();
}
public override string ToString() {
return "{0} ({1})".F(Key, Title);
}
}
}