mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
* oops
* fixes serialization il
* copytest
* typo & misc fixes
* 139 moment
* boxing
* mesa dum
* stuff
* goodbye bad friend
* last commit before the big (4) rewrite
* adds datanodes
* kills yamlobjserializer in favor of the new system
* adds more serializers, actually implements them & removes most of the last of the old system
* changed yamlfieldattribute namespace
* adds back iselfserialize
* refactors consts&flags
* renames everything to data(field/definition)
* adds afterserialization
* help
* dataclassgen
* fuggen help me mannen
* Fix most errors on content
* Fix engine errors except map loader
* maploader & misc fix
* misc fixes
* thing
* help
* refactors datanodes
* help me mannen
* Separate ITypeSerializer into reader and writer
* Convert all type serializers
* priority
* adds alot
* il fixes
* adds robustgen
* argh
* adds array & enum serialization
* fixes dataclasses
* adds vec2i / misc fixes
* fixes inheritance
* a very notcursed todo
* fixes some custom dataclasses
* push dis
* Remove data classes
* boutta box
* yes
* Add angle and regex serializer tests
* Make TypeSerializerTest abstract
* sets up ioc etc
* remove pushinheritance
* fixes
* Merge fixes, fix yaml hot reloading
* General fixes2
* Make enum serialization ignore case
* Fix the tag not being copied in data nodes
* Fix not properly serializing flag enums
* Fix component serialization on startup
* Implement ValueDataNode ToString
* Serialization IL fixes, fix return and string equality
* Remove async from prototype manager
* Make serializing unsupported node as enum exception more descriptive
* Fix serv3 tryread casting to serializer instead of reader
* Add constructor for invalid node type exception
* Temporary fix for SERV3: Turn populate delegate into regular code
* Fix not copying the data of non primitive types
* Fix not using the data definition found in copying
* Make ISerializationHooks require explicit implementations
* Add test for serialization inheritance
* Improve IsOverridenIn method
* Fix error message when a data definition is null
* Add method to cast a read value in Serv3Manager
* Rename IServ3Manager to ISerializationManager
* Rename usages of serv3manager, add generic copy method
* Fix IL copy method lookup
* Rename old usages of serv3manager
* Add ITypeCopier
* resistance is futile
* we will conquer this codebase
* Add copy method to all serializers
* Make primitive mismatch error message more descriptive
* bing bong im going to freacking heck
* oopsie moment
* hello are you interested in my wares
* does generic serializers under new architecture
* Convert every non generic serializer to the new format, general fixes
* Update usgaes of generic serializers, cleanup
* does some pushinheritance logic
* finishes pushinheritance FRAMEWORK
* shed
* Add box2, color and component registry serializer tests
* Create more deserialized types and store prototypes with their deserialized results
* Fixes and serializer updates
* Add serialization manager extensions
* adds pushinheritance
* Update all prototypes to have a parent and have consistent id/parent properties
* Fix grammar component serialization
* Add generic serializer tests
* thonk
* Add array serializer test
* Replace logger warning calls with exceptions
* fixes
* Move redundant methods to serialization manager extensions, cleanup
* Add array serialization
* fixes context
* more fixes
* argh
* inheritance
* this should do it
* fixes
* adds copiers & fixes some stuff
* copiers use context v1
* finishing copy context
* more context fixes
* Test fixes
* funky maps
* Fix server user interface component serialization
* Fix value tuple serialization
* Add copying for value types and arrays. Fix copy internal for primitives, enums and strings
* fixes
* fixes more stuff
* yes
* Make abstract/interface skips debugs instead of warnings
* Fix typo
* Make some dictionaries readonly
* Add checks for the serialization manager initializing and already being initialized
* Add base type required and usage for MeansDataDefinition and ImplicitDataDefinitionForInheritorsAttribute
* copy by ref
* Fix exception wording
* Update data field required summary with the new forbidden docs
* Use extension in map loader
* wanna erp
* Change serializing to not use il temporarily
* Make writing work with nullable types
* pushing
* check
* cuddling slaps HARD
* Add serialization priority test
* important fix
* a serialization thing
* serializer moment
* Add validation for some type serializers
* adds context
* moar context
* fixes
* Do the thing for appearance
* yoo lmao
* push haha pp
* Temporarily make copy delegate regular c# code
* Create deserialized component registry to handle not inheriting conflicting references
* YAML LINTER BABY
* ayes
* Fix sprite component norot not being default true like in latest master
* Remove redundant todos
* Add summary doc to every ISerializationManager method
* icon fixes
* Add skip hook argument to readers and copiers
* Merge fixes
* Fix ordering of arguments in read and copy reflection call
* Fix user interface components deserialization
* pew pew
* i am going to HECK
* Add MustUseReturnValue to copy-over methods
* Make serialization log calls use the same sawmill
* gamin
* Fix doc errors in ISerializationManager.cs
* goodbye brave soldier
* fixes
* WIP merge fixes and entity serialization
* aaaaaaaaaaaaaaa
* aaaaaaaaaaaaaaa
* adds inheritancebehaviour
* test/datafield fixes
* forgot that one
* adds more verbose validation
* This fixes the YAML hot reloading
* Replace yield break with Enumerable.Empty
* adds copiers
* aaaaaaaaaaaaa
* array fix
priority fix
misc fixes
* fix(?)
* fix.
* funny map serialization (wip)
* funny map serialization (wip)
* Add TODO
* adds proper info the validation
* Make yaml linter 5 times faster (~80% less execution time)
* Improves the error message for missing fields in the linter
* Include component name in unknown component type error node
* adds alwaysrelevant usa
* fixes mapsaving
* moved surpressor to analyzers proj
* warning cleanup & moves surpressor
* removes old msbuild targets
* Revert "Make yaml linter 5 times faster (~80% less execution time)"
This reverts commit 2ee4cc2c26.
* Add serialization to RobustServerSimulation and mock reflection methods
Fixes container tests
* Fix nullability warnings
* Improve yaml linter message feedback
* oops moment
* Add IEquatable, IComparable, ToString and operators to DataPosition
Rename it to NodeMark
Make it a readonly struct
* Remove try catch from enum parsing
* Make dependency management in serialization less bad
* Make dependencies an argument instead of a property on the serialization manager
* Clean up type serializers
* Improve validation messages and resourc epath checking
* Fix sprite error message
* reached perfection
Co-authored-by: Paul <ritter.paul1+git@googlemail.com>
Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>
Co-authored-by: Vera Aguilera Puerto <zddm@outlook.es>
877 lines
29 KiB
C#
877 lines
29 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Diagnostics;
|
||
using System.Diagnostics.CodeAnalysis;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Runtime.InteropServices;
|
||
using System.Text;
|
||
using JetBrains.Annotations;
|
||
using Robust.Client.UserInterface;
|
||
using Robust.Shared.Console;
|
||
using Robust.Shared.ContentPack;
|
||
using Robust.Shared.Input;
|
||
using Robust.Shared.Input.Binding;
|
||
using Robust.Shared.IoC;
|
||
using Robust.Shared.Localization;
|
||
using Robust.Shared.Log;
|
||
using Robust.Shared.Map;
|
||
using Robust.Shared.Maths;
|
||
using Robust.Shared.Reflection;
|
||
using Robust.Shared.Serialization;
|
||
using Robust.Shared.Serialization.Manager;
|
||
using Robust.Shared.Serialization.Markdown;
|
||
using Robust.Shared.Utility;
|
||
using Robust.Shared.ViewVariables;
|
||
using YamlDotNet.Core;
|
||
using YamlDotNet.RepresentationModel;
|
||
using static Robust.Client.Input.Keyboard;
|
||
|
||
namespace Robust.Client.Input
|
||
{
|
||
internal class InputManager : IInputManager
|
||
{
|
||
// This is for both userdata and resources.
|
||
private const string KeybindsPath = "/keybinds.yml";
|
||
|
||
[ViewVariables] public bool Enabled { get; set; } = true;
|
||
|
||
[ViewVariables] public virtual Vector2 MouseScreenPosition => Vector2.Zero;
|
||
|
||
[Dependency] private readonly IResourceManager _resourceMan = default!;
|
||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||
[Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManagerInternal = default!;
|
||
|
||
private readonly List<KeyBindingRegistration> _defaultRegistrations = new();
|
||
|
||
private readonly Dictionary<BoundKeyFunction, InputCmdHandler> _commands =
|
||
new();
|
||
|
||
private readonly Dictionary<BoundKeyFunction, List<KeyBinding>> _bindingsByFunction
|
||
= new();
|
||
|
||
// For knowing what to write to config.
|
||
private readonly HashSet<BoundKeyFunction> _modifiedKeyFunctions = new();
|
||
|
||
[ViewVariables] private readonly List<KeyBinding> _bindings = new();
|
||
private readonly bool[] _keysPressed = new bool[256];
|
||
|
||
/// <inheritdoc />
|
||
[ViewVariables]
|
||
public BoundKeyMap NetworkBindMap { get; private set; } = default!;
|
||
|
||
/// <inheritdoc />
|
||
[ViewVariables]
|
||
public IInputContextContainer Contexts { get; } = new InputContextContainer();
|
||
|
||
/// <inheritdoc />
|
||
public event Func<BoundKeyEventArgs, bool>? UIKeyBindStateChanged;
|
||
|
||
/// <inheritdoc />
|
||
public event Action<BoundKeyEventArgs>? KeyBindStateChanged;
|
||
|
||
public IEnumerable<BoundKeyFunction> DownKeyFunctions => _bindings
|
||
.Where(x => x.State == BoundKeyState.Down)
|
||
.Select(x => x.Function)
|
||
.ToList();
|
||
|
||
public virtual string GetKeyName(Key key)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
public string GetKeyFunctionButtonString(BoundKeyFunction function)
|
||
{
|
||
if (!TryGetKeyBinding(function, out var bind))
|
||
{
|
||
return Loc.GetString("<not bound>");
|
||
}
|
||
|
||
return bind.GetKeyString();
|
||
}
|
||
|
||
public IEnumerable<IKeyBinding> AllBindings => _bindings;
|
||
public event KeyEventAction? FirstChanceOnKeyEvent;
|
||
public event Action<IKeyBinding>? OnKeyBindingAdded;
|
||
public event Action<IKeyBinding>? OnKeyBindingRemoved;
|
||
|
||
/// <inheritdoc />
|
||
public void Initialize()
|
||
{
|
||
NetworkBindMap = new BoundKeyMap(_reflectionManager);
|
||
NetworkBindMap.PopulateKeyFunctionsMap();
|
||
|
||
EngineContexts.SetupContexts(Contexts);
|
||
|
||
Contexts.ContextChanged += OnContextChanged;
|
||
|
||
var path = new ResourcePath(KeybindsPath);
|
||
if (_resourceMan.UserData.Exists(path))
|
||
{
|
||
LoadKeyFile(path, true);
|
||
}
|
||
|
||
if (_resourceMan.ContentFileExists(path))
|
||
{
|
||
LoadKeyFile(path, false);
|
||
}
|
||
}
|
||
|
||
public void SaveToUserData()
|
||
{
|
||
var mapping = new MappingDataNode();
|
||
var serializationManager = IoCManager.Resolve<ISerializationManager>();
|
||
|
||
var modifiedBindings = _modifiedKeyFunctions
|
||
.Select(p => _bindingsByFunction[p])
|
||
.SelectMany(p => p)
|
||
.Select(p => new KeyBindingRegistration
|
||
{
|
||
Function = p.Function,
|
||
BaseKey = p.BaseKey,
|
||
Mod1 = p.Mod1,
|
||
Mod2 = p.Mod2,
|
||
Mod3 = p.Mod3,
|
||
Priority = p.Priority,
|
||
Type = p.BindingType,
|
||
CanFocus = p.CanFocus,
|
||
CanRepeat = p.CanRepeat,
|
||
AllowSubCombs = p.AllowSubCombs
|
||
}).ToArray();
|
||
|
||
var leaveEmpty = _modifiedKeyFunctions
|
||
.Where(p => _bindingsByFunction[p].Count == 0)
|
||
.ToArray();
|
||
|
||
mapping.AddNode("version", new ValueDataNode("1"));
|
||
mapping.AddNode("binds", serializationManager.WriteValue(modifiedBindings));
|
||
mapping.AddNode("leaveEmpty", serializationManager.WriteValue(leaveEmpty));
|
||
|
||
var path = new ResourcePath(KeybindsPath);
|
||
using var writer = new StreamWriter(_resourceMan.UserData.Create(path));
|
||
var stream = new YamlStream {new(mapping.ToMappingNode())};
|
||
stream.Save(new YamlMappingFix(new Emitter(writer)), false);
|
||
}
|
||
|
||
private void OnContextChanged(object? sender, ContextChangedEventArgs args)
|
||
{
|
||
// keyup any commands that are not in the new contexts, because it will not exist in the new context and get filtered. Luckily
|
||
// the diff does not have to be symmetrical, otherwise instead of 'A \ B' we allocate all the things with '(A \ B) ∪ (B \ A)'
|
||
// It should be OK to artificially keyup these, because in the future the organic keyup will be blocked (either the context
|
||
// does not have the binding, or the double keyup check in UpBind will block it).
|
||
if (args.OldContext == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
IEnumerable<BoundKeyFunction> enumerable = args.OldContext;
|
||
if (args.NewContext != null)
|
||
{
|
||
enumerable = enumerable.Except(args.NewContext);
|
||
}
|
||
|
||
foreach (var function in enumerable)
|
||
{
|
||
var bind = _bindings.Find(binding => binding.Function == function);
|
||
if (bind == null || bind.State == BoundKeyState.Up)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
SetBindState(bind, BoundKeyState.Up);
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public void KeyDown(KeyEventArgs args)
|
||
{
|
||
if (!Enabled || args.Key == Key.Unknown)
|
||
{
|
||
return;
|
||
}
|
||
|
||
FirstChanceOnKeyEvent?.Invoke(args, args.IsRepeat ? KeyEventType.Repeat : KeyEventType.Down);
|
||
|
||
if (args.Handled)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_keysPressed[(int) args.Key] = true;
|
||
|
||
PackedKeyCombo matchedCombo = default;
|
||
|
||
var bindsDown = new List<KeyBinding>();
|
||
var hasCanFocus = false;
|
||
var hasAllowSubCombs = false;
|
||
|
||
// bindings are ordered with larger combos before single key bindings so combos have priority.
|
||
foreach (var binding in _bindings)
|
||
{
|
||
// check if our binding is even in the active context
|
||
if (!Contexts.ActiveContext.FunctionExistsHierarchy(binding.Function))
|
||
continue;
|
||
|
||
if (PackedMatchesPressedState(binding.PackedKeyCombo))
|
||
{
|
||
// this statement *should* always be true first
|
||
// Keep triggering keybinds of the same PackedKeyCombo until Handled or no bindings left
|
||
if ((matchedCombo == default || binding.PackedKeyCombo == matchedCombo) &&
|
||
PackedContainsKey(binding.PackedKeyCombo, args.Key))
|
||
{
|
||
matchedCombo = binding.PackedKeyCombo;
|
||
|
||
bindsDown.Add(binding);
|
||
|
||
hasCanFocus |= binding.CanFocus;
|
||
hasAllowSubCombs |= binding.AllowSubCombs;
|
||
|
||
}
|
||
else if (PackedIsSubPattern(matchedCombo, binding.PackedKeyCombo))
|
||
{
|
||
if (hasAllowSubCombs)
|
||
{
|
||
bindsDown.Add(binding);
|
||
}
|
||
else
|
||
{
|
||
// kill any lower level matches
|
||
UpBind(binding);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
var uiOnly = false;
|
||
if (hasCanFocus)
|
||
{
|
||
uiOnly = _userInterfaceManagerInternal.HandleCanFocusDown(MouseScreenPosition);
|
||
}
|
||
|
||
foreach (var binding in bindsDown)
|
||
{
|
||
if (DownBind(binding, uiOnly, args.IsRepeat))
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public void KeyUp(KeyEventArgs args)
|
||
{
|
||
if (args.Key == Key.Unknown)
|
||
{
|
||
return;
|
||
}
|
||
|
||
FirstChanceOnKeyEvent?.Invoke(args, KeyEventType.Up);
|
||
|
||
var hasCanFocus = false;
|
||
foreach (var binding in _bindings)
|
||
{
|
||
// check if our binding is even in the active context
|
||
if (!Contexts.ActiveContext.FunctionExistsHierarchy(binding.Function))
|
||
continue;
|
||
|
||
if (PackedContainsKey(binding.PackedKeyCombo, args.Key) &&
|
||
PackedMatchesPressedState(binding.PackedKeyCombo))
|
||
{
|
||
hasCanFocus |= binding.CanFocus;
|
||
UpBind(binding);
|
||
}
|
||
}
|
||
|
||
_keysPressed[(int) args.Key] = false;
|
||
|
||
if (hasCanFocus)
|
||
{
|
||
_userInterfaceManagerInternal.HandleCanFocusUp();
|
||
}
|
||
}
|
||
|
||
private bool DownBind(KeyBinding binding, bool uiOnly, bool isRepeat)
|
||
{
|
||
if (binding.State == BoundKeyState.Down)
|
||
{
|
||
if (isRepeat)
|
||
{
|
||
if (binding.CanRepeat)
|
||
{
|
||
return SetBindState(binding, BoundKeyState.Down, uiOnly);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
if (binding.BindingType == KeyBindingType.Toggle)
|
||
{
|
||
return SetBindState(binding, BoundKeyState.Up);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
return SetBindState(binding, BoundKeyState.Down, uiOnly);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private void UpBind(KeyBinding binding)
|
||
{
|
||
if (binding.State == BoundKeyState.Up || binding.BindingType == KeyBindingType.Toggle)
|
||
{
|
||
return;
|
||
}
|
||
|
||
SetBindState(binding, BoundKeyState.Up);
|
||
}
|
||
|
||
private bool SetBindState(KeyBinding binding, BoundKeyState state, bool uiOnly = false)
|
||
{
|
||
binding.State = state;
|
||
|
||
var eventArgs = new BoundKeyEventArgs(binding.Function, binding.State,
|
||
new ScreenCoordinates(MouseScreenPosition), binding.CanFocus);
|
||
|
||
var handled = UIKeyBindStateChanged?.Invoke(eventArgs);
|
||
if (state == BoundKeyState.Up
|
||
|| !(handled == true || eventArgs.Handled)
|
||
&& !uiOnly)
|
||
{
|
||
var cmd = GetInputCommand(binding.Function);
|
||
// TODO: Allow input commands to still get forwarded to server if necessary.
|
||
if (cmd != null)
|
||
{
|
||
if (state == BoundKeyState.Up)
|
||
{
|
||
cmd.Disabled(null);
|
||
}
|
||
else
|
||
{
|
||
cmd.Enabled(null);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
KeyBindStateChanged?.Invoke(eventArgs);
|
||
}
|
||
}
|
||
|
||
return eventArgs.Handled;
|
||
}
|
||
|
||
private bool PackedMatchesPressedState(PackedKeyCombo packed)
|
||
{
|
||
var (baseKey, mod1, mod2, mod3) = packed;
|
||
|
||
if (!_keysPressed[(int) baseKey]) return false;
|
||
if (mod1 != Key.Unknown && !_keysPressed[(int) mod1]) return false;
|
||
if (mod2 != Key.Unknown && !_keysPressed[(int) mod2]) return false;
|
||
if (mod3 != Key.Unknown && !_keysPressed[(int) mod3]) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
private static bool PackedContainsKey(PackedKeyCombo packed, Key key)
|
||
{
|
||
var (baseKey, mod1, mod2, mod3) = packed;
|
||
|
||
if (baseKey == key) return true;
|
||
if (mod1 != Key.Unknown && mod1 == key) return true;
|
||
if (mod2 != Key.Unknown && mod2 == key) return true;
|
||
if (mod3 != Key.Unknown && mod3 == key) return true;
|
||
|
||
return false;
|
||
}
|
||
|
||
private static bool PackedIsSubPattern(PackedKeyCombo packedCombo, PackedKeyCombo subPackedCombo)
|
||
{
|
||
for (var i = 0; i < 32; i += 8)
|
||
{
|
||
var key = (Key) ((subPackedCombo.Packed >> i) & 0b_1111_1111);
|
||
if (key != Key.Unknown && !PackedContainsKey(packedCombo, key))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private void LoadKeyFile(ResourcePath file, bool userData)
|
||
{
|
||
TextReader reader;
|
||
if (userData)
|
||
{
|
||
reader = _resourceMan.UserData.OpenText(file);
|
||
}
|
||
else
|
||
{
|
||
reader = _resourceMan.ContentFileReadText(file);
|
||
}
|
||
|
||
var yamlStream = new YamlStream();
|
||
yamlStream.Load(reader);
|
||
|
||
var mapping = (YamlMappingNode) yamlStream.Documents[0].RootNode;
|
||
|
||
var serializationManager = IoCManager.Resolve<ISerializationManager>();
|
||
var robustMapping = mapping.ToDataNode() as MappingDataNode;
|
||
if (robustMapping == null) throw new InvalidOperationException();
|
||
|
||
if (robustMapping.TryGetNode("binds", out var BaseKeyRegsNode))
|
||
{
|
||
var baseKeyRegs = serializationManager.ReadValueOrThrow<KeyBindingRegistration[]>(BaseKeyRegsNode);
|
||
|
||
foreach (var reg in baseKeyRegs)
|
||
{
|
||
if (!NetworkBindMap.FunctionExists(reg.Function.FunctionName))
|
||
{
|
||
Logger.ErrorS("input", "Key function in {0} does not exist: '{1}'", file,
|
||
reg.Function.FunctionName);
|
||
continue;
|
||
}
|
||
|
||
if (!userData)
|
||
{
|
||
_defaultRegistrations.Add(reg);
|
||
|
||
if (_modifiedKeyFunctions.Contains(reg.Function))
|
||
{
|
||
// Don't read key functions from preset files that have been modified.
|
||
// So that we don't bulldoze a user's saved preferences.
|
||
continue;
|
||
}
|
||
}
|
||
|
||
RegisterBinding(reg, markModified: userData);
|
||
}
|
||
}
|
||
|
||
if (userData && robustMapping.TryGetNode("leaveEmpty", out var node))
|
||
{
|
||
var leaveEmpty = serializationManager.ReadValueOrThrow<BoundKeyFunction[]>(node);
|
||
|
||
if (leaveEmpty.Length > 0)
|
||
{
|
||
// Adding to _modifiedKeyFunctions means that these keybinds won't be loaded from the base file.
|
||
// Because they've been explicitly cleared.
|
||
_modifiedKeyFunctions.UnionWith(leaveEmpty);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public IKeyBinding RegisterBinding(BoundKeyFunction function, KeyBindingType bindingType,
|
||
Key baseKey, Key? mod1, Key? mod2, Key? mod3)
|
||
{
|
||
var binding = new KeyBinding(this, function, bindingType, baseKey, false, false, false,
|
||
0, mod1 ?? Key.Unknown, mod2 ?? Key.Unknown, mod3 ?? Key.Unknown);
|
||
|
||
RegisterBinding(binding);
|
||
|
||
return binding;
|
||
}
|
||
|
||
public IKeyBinding RegisterBinding(in KeyBindingRegistration reg, bool markModified = true)
|
||
{
|
||
var binding = new KeyBinding(this, reg.Function, reg.Type, reg.BaseKey, reg.CanFocus, reg.CanRepeat,
|
||
reg.AllowSubCombs, reg.Priority, reg.Mod1, reg.Mod2, reg.Mod3);
|
||
|
||
RegisterBinding(binding, markModified);
|
||
|
||
return binding;
|
||
}
|
||
|
||
public void RemoveBinding(IKeyBinding binding, bool markModified = true)
|
||
{
|
||
var bindings = _bindingsByFunction[binding.Function];
|
||
var cast = (KeyBinding) binding;
|
||
if (!bindings.Remove(cast))
|
||
{
|
||
// Keybind does not exist.
|
||
return;
|
||
}
|
||
|
||
if (markModified)
|
||
{
|
||
_modifiedKeyFunctions.Add(binding.Function);
|
||
}
|
||
|
||
_bindings.Remove(cast);
|
||
OnKeyBindingRemoved?.Invoke(binding);
|
||
}
|
||
|
||
private void RegisterBinding(KeyBinding binding, bool markModified = true)
|
||
{
|
||
// we sort larger combos first so they take priority over smaller (single key) combos,
|
||
// so they get processed first in KeyDown and such.
|
||
var pos = _bindings.BinarySearch(binding, KeyBinding.ProcessPriorityComparer);
|
||
if (pos < 0)
|
||
{
|
||
pos = ~pos;
|
||
}
|
||
|
||
if (markModified)
|
||
{
|
||
_modifiedKeyFunctions.Add(binding.Function);
|
||
}
|
||
|
||
_bindings.Insert(pos, binding);
|
||
_bindingsByFunction.GetOrNew(binding.Function).Add(binding);
|
||
OnKeyBindingAdded?.Invoke(binding);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public IKeyBinding GetKeyBinding(BoundKeyFunction function)
|
||
{
|
||
if (TryGetKeyBinding(function, out var binding))
|
||
{
|
||
return binding;
|
||
}
|
||
|
||
throw new KeyNotFoundException($"No keys are bound for function '{function}'");
|
||
}
|
||
|
||
public IReadOnlyList<IKeyBinding> GetKeyBindings(BoundKeyFunction function)
|
||
{
|
||
return _bindingsByFunction.GetOrNew(function);
|
||
}
|
||
|
||
public void ResetBindingsFor(BoundKeyFunction function)
|
||
{
|
||
foreach (var binding in GetKeyBindings(function).ToArray())
|
||
{
|
||
RemoveBinding(binding);
|
||
}
|
||
|
||
// Mark as unmodified.
|
||
_modifiedKeyFunctions.Remove(function);
|
||
|
||
foreach (var defaultBinding in _defaultRegistrations.Where(p => p.Function == function))
|
||
{
|
||
RegisterBinding(defaultBinding, markModified: false);
|
||
}
|
||
}
|
||
|
||
public void ResetAllBindings()
|
||
{
|
||
foreach (var modified in _modifiedKeyFunctions.ToArray())
|
||
{
|
||
ResetBindingsFor(modified);
|
||
}
|
||
}
|
||
|
||
public bool IsKeyFunctionModified(BoundKeyFunction function)
|
||
{
|
||
return _modifiedKeyFunctions.Contains(function);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public bool TryGetKeyBinding(BoundKeyFunction function, [NotNullWhen(true)] out IKeyBinding? binding)
|
||
{
|
||
if (!_bindingsByFunction.TryGetValue(function, out var bindings))
|
||
{
|
||
binding = null;
|
||
return false;
|
||
}
|
||
|
||
binding = bindings.FirstOrDefault();
|
||
return binding != null;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public InputCmdHandler? GetInputCommand(BoundKeyFunction function)
|
||
{
|
||
if (_commands.TryGetValue(function, out var val))
|
||
{
|
||
return val;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public void SetInputCommand(BoundKeyFunction function, InputCmdHandler? cmdHandler)
|
||
{
|
||
if (cmdHandler == null)
|
||
{
|
||
_commands.Remove(function);
|
||
}
|
||
else
|
||
{
|
||
_commands[function] = cmdHandler;
|
||
}
|
||
}
|
||
|
||
[DebuggerDisplay("KeyBinding {" + nameof(Function) + "}")]
|
||
private class KeyBinding : IKeyBinding
|
||
{
|
||
private readonly InputManager _inputManager;
|
||
|
||
[ViewVariables] public BoundKeyState State { get; set; }
|
||
public PackedKeyCombo PackedKeyCombo { get; }
|
||
[ViewVariables] public BoundKeyFunction Function { get; }
|
||
[ViewVariables] public KeyBindingType BindingType { get; }
|
||
|
||
[ViewVariables] public Key BaseKey => PackedKeyCombo.BaseKey;
|
||
[ViewVariables] public Key Mod1 => PackedKeyCombo.Mod1;
|
||
[ViewVariables] public Key Mod2 => PackedKeyCombo.Mod2;
|
||
[ViewVariables] public Key Mod3 => PackedKeyCombo.Mod3;
|
||
|
||
/// <summary>
|
||
/// Whether the BoundKey can change the focused control.
|
||
/// </summary>
|
||
[ViewVariables]
|
||
public bool CanFocus { get; internal set; }
|
||
|
||
/// <summary>
|
||
/// Whether the BoundKey still triggers while held down.
|
||
/// </summary>
|
||
[ViewVariables]
|
||
public bool CanRepeat { get; internal set; }
|
||
|
||
/// <summary>
|
||
/// Whether the Bound Key Combination allows Sub Combinations of it to trigger.
|
||
/// </summary>
|
||
[ViewVariables]
|
||
public bool AllowSubCombs { get; internal set; }
|
||
|
||
[ViewVariables] public int Priority { get; internal set; }
|
||
|
||
public KeyBinding(InputManager inputManager, BoundKeyFunction function,
|
||
KeyBindingType bindingType,
|
||
Key baseKey,
|
||
bool canFocus, bool canRepeat, bool allowSubCombs, int priority, Key mod1 = Key.Unknown,
|
||
Key mod2 = Key.Unknown,
|
||
Key mod3 = Key.Unknown)
|
||
{
|
||
Function = function;
|
||
BindingType = bindingType;
|
||
CanFocus = canFocus;
|
||
CanRepeat = canRepeat;
|
||
AllowSubCombs = allowSubCombs;
|
||
Priority = priority;
|
||
_inputManager = inputManager;
|
||
|
||
PackedKeyCombo = new PackedKeyCombo(baseKey, mod1, mod2, mod3);
|
||
}
|
||
|
||
public string GetKeyString()
|
||
{
|
||
var (baseKey, mod1, mod2, mod3) = PackedKeyCombo;
|
||
|
||
var sb = new StringBuilder();
|
||
|
||
if (mod3 != Key.Unknown)
|
||
{
|
||
sb.AppendFormat("{0}+", _inputManager.GetKeyName(mod3));
|
||
}
|
||
|
||
if (mod2 != Key.Unknown)
|
||
{
|
||
sb.AppendFormat("{0}+", _inputManager.GetKeyName(mod2));
|
||
}
|
||
|
||
if (mod1 != Key.Unknown)
|
||
{
|
||
sb.AppendFormat("{0}+", _inputManager.GetKeyName(mod1));
|
||
}
|
||
|
||
sb.Append(_inputManager.GetKeyName(baseKey));
|
||
|
||
return sb.ToString();
|
||
}
|
||
|
||
private sealed class ProcessPriorityRelationalComparer : IComparer<KeyBinding>
|
||
{
|
||
public int Compare(KeyBinding? x, KeyBinding? y)
|
||
{
|
||
if (ReferenceEquals(x, y)) return 0;
|
||
if (ReferenceEquals(null, y)) return 1;
|
||
if (ReferenceEquals(null, x)) return -1;
|
||
var cmp = y.PackedKeyCombo.Packed.CompareTo(x.PackedKeyCombo.Packed);
|
||
// Higher priority is first in the list so gets to go first.
|
||
return cmp != 0 ? cmp : y.Priority.CompareTo(x.Priority);
|
||
}
|
||
}
|
||
|
||
public override string ToString()
|
||
{
|
||
var sb = new StringBuilder();
|
||
sb.AppendFormat("{0}: {1}", Function.FunctionName, BaseKey);
|
||
if (Mod1 != Key.Unknown)
|
||
{
|
||
sb.AppendFormat("+{0}", Mod1);
|
||
if (Mod2 != Key.Unknown)
|
||
{
|
||
sb.AppendFormat("+{0}", Mod2);
|
||
if (Mod3 != Key.Unknown)
|
||
{
|
||
sb.AppendFormat("+{0}", Mod3);
|
||
}
|
||
}
|
||
}
|
||
|
||
return sb.ToString();
|
||
}
|
||
|
||
public static IComparer<KeyBinding> ProcessPriorityComparer { get; } =
|
||
new ProcessPriorityRelationalComparer();
|
||
}
|
||
|
||
[StructLayout(LayoutKind.Explicit)]
|
||
private readonly struct PackedKeyCombo : IEquatable<PackedKeyCombo>
|
||
{
|
||
[FieldOffset(0)] public readonly int Packed;
|
||
[FieldOffset(0)] public readonly Key Mod3;
|
||
[FieldOffset(1)] public readonly Key Mod2;
|
||
[FieldOffset(2)] public readonly Key Mod1;
|
||
[FieldOffset(3)] public readonly Key BaseKey;
|
||
|
||
public PackedKeyCombo(Key baseKey,
|
||
Key mod1 = Key.Unknown,
|
||
Key mod2 = Key.Unknown,
|
||
Key mod3 = Key.Unknown)
|
||
{
|
||
if (baseKey == Key.Unknown)
|
||
throw new ArgumentOutOfRangeException(nameof(baseKey), baseKey, "Cannot bind Unknown key.");
|
||
|
||
// Modifiers are sorted so that the higher key values are lower in the integer bytes.
|
||
// Unknown is zero so at the very "top".
|
||
// More modifiers thus takes precedent with that sort in RegisterBinding,
|
||
// and order only matters for amount of modifiers, not the modifiers themselves,
|
||
// Use a simplistic bubble sort to sort the key modifiers.
|
||
if (mod1 < mod2) (mod1, mod2) = (mod2, mod1);
|
||
if (mod2 < mod3) (mod2, mod3) = (mod3, mod2);
|
||
if (mod1 < mod2) (mod1, mod2) = (mod2, mod1);
|
||
|
||
// Working around the fact that C# is not aware of Explicit layout
|
||
// and requires all struct fields be initialized.
|
||
Packed = default;
|
||
|
||
BaseKey = baseKey;
|
||
Mod1 = mod1;
|
||
Mod2 = mod2;
|
||
Mod3 = mod3;
|
||
}
|
||
|
||
public void Deconstruct(out Key baseKey, out Key mod1, out Key mod2, out Key mod3)
|
||
{
|
||
baseKey = BaseKey;
|
||
mod1 = Mod1;
|
||
mod2 = Mod2;
|
||
mod3 = Mod3;
|
||
}
|
||
|
||
public bool Equals(PackedKeyCombo other)
|
||
{
|
||
return Packed == other.Packed;
|
||
}
|
||
|
||
public override bool Equals(object? obj)
|
||
{
|
||
return obj is PackedKeyCombo other && Equals(other);
|
||
}
|
||
|
||
public override int GetHashCode()
|
||
{
|
||
return Packed;
|
||
}
|
||
|
||
public static bool operator ==(PackedKeyCombo left, PackedKeyCombo right)
|
||
{
|
||
return left.Equals(right);
|
||
}
|
||
|
||
public static bool operator !=(PackedKeyCombo left, PackedKeyCombo right)
|
||
{
|
||
return !left.Equals(right);
|
||
}
|
||
}
|
||
}
|
||
|
||
public enum KeyBindingType : byte
|
||
{
|
||
Unknown = 0,
|
||
State,
|
||
Toggle,
|
||
}
|
||
|
||
public enum CommandState : byte
|
||
{
|
||
Unknown = 0,
|
||
Enabled,
|
||
Disabled,
|
||
}
|
||
|
||
[UsedImplicitly]
|
||
internal class BindCommand : IConsoleCommand
|
||
{
|
||
public string Command => "bind";
|
||
public string Description => "Binds an input key to an input command.";
|
||
public string Help => "bind <KeyName> <BindMode> <InputCommand>";
|
||
|
||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||
{
|
||
if (args.Length < 3)
|
||
{
|
||
shell.WriteLine("Too few arguments.");
|
||
return;
|
||
}
|
||
|
||
if (args.Length > 3)
|
||
{
|
||
shell.WriteLine("Too many arguments.");
|
||
return;
|
||
}
|
||
|
||
var keyName = args[0];
|
||
|
||
if (!Enum.TryParse(typeof(Key), keyName, true, out var keyIdObj))
|
||
{
|
||
shell.WriteLine($"Key '{keyName}' is unrecognized.");
|
||
return;
|
||
}
|
||
|
||
var keyId = (Key) keyIdObj!;
|
||
|
||
if (!Enum.TryParse(typeof(KeyBindingType), args[1], true, out var keyModeObj))
|
||
{
|
||
shell.WriteLine($"BindMode '{args[1]}' is unrecognized.");
|
||
return;
|
||
}
|
||
|
||
var keyMode = (KeyBindingType) keyModeObj!;
|
||
|
||
var inputCommand = args[2];
|
||
|
||
var inputMan = IoCManager.Resolve<IInputManager>();
|
||
|
||
var registration = new KeyBindingRegistration
|
||
{
|
||
Function = new BoundKeyFunction(inputCommand),
|
||
BaseKey = keyId,
|
||
Type = keyMode
|
||
};
|
||
|
||
inputMan.RegisterBinding(registration);
|
||
}
|
||
}
|
||
|
||
[UsedImplicitly]
|
||
internal class SaveBindCommand : IConsoleCommand
|
||
{
|
||
public string Command => "svbind";
|
||
public string Description => "";
|
||
public string Help => "";
|
||
|
||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||
{
|
||
IoCManager.Resolve<IInputManager>()
|
||
.SaveToUserData();
|
||
}
|
||
}
|
||
}
|