mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-11 12:14:30 +08:00
- 修复 InputBindingStore 的线程安全使用说明并消除 GodotInputBindingCodec 的重复键码计算\n- 修复 GodotInputMapBackend 在全量重置时未移除运行时新增动作的语义偏差并补回归测试\n- 更新 input-system-godot-integration 的 tracking 与 trace,补录 PR review follow-up 验证结果
193 lines
6.2 KiB
C#
193 lines
6.2 KiB
C#
// Copyright (c) 2025-2026 GeWuYou
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
using GFramework.Game.Abstractions.Input;
|
|
|
|
namespace GFramework.Game.Input;
|
|
|
|
/// <summary>
|
|
/// 提供基于内存快照的默认输入绑定存储实现。
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 该实现聚焦于框架级动作绑定管理语义:默认值恢复、主绑定替换、冲突交换与快照导入导出。
|
|
/// 它不依赖具体宿主输入事件,适合作为 `Game` 层默认运行时与单元测试基线。
|
|
/// 该类型内部使用普通 `Dictionary` / `List` 保存可变状态,不提供额外同步原语。
|
|
/// 宿主应在同一输入线程或受控的串行配置阶段访问它;如果存在跨线程读写需求,应由外层协调同步。
|
|
/// </remarks>
|
|
public sealed class InputBindingStore : IInputBindingStore
|
|
{
|
|
private readonly Dictionary<string, List<InputBindingDescriptor>> _defaultBindings;
|
|
private readonly Dictionary<string, List<InputBindingDescriptor>> _currentBindings;
|
|
|
|
/// <summary>
|
|
/// 初始化输入绑定存储。
|
|
/// </summary>
|
|
/// <param name="defaultSnapshot">默认绑定快照。</param>
|
|
public InputBindingStore(InputBindingSnapshot defaultSnapshot)
|
|
{
|
|
_defaultBindings = ToDictionary(defaultSnapshot);
|
|
_currentBindings = CloneDictionary(_defaultBindings);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public InputActionBinding GetBindings(string actionName)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(actionName);
|
|
|
|
return _currentBindings.TryGetValue(actionName, out var bindings)
|
|
? new InputActionBinding(actionName, bindings.ToArray())
|
|
: new InputActionBinding(actionName, Array.Empty<InputBindingDescriptor>());
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public InputBindingSnapshot ExportSnapshot()
|
|
{
|
|
var actions = _currentBindings
|
|
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
|
|
.Select(static pair => new InputActionBinding(pair.Key, pair.Value.ToArray()))
|
|
.ToArray();
|
|
|
|
return new InputBindingSnapshot(actions);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void ImportSnapshot(InputBindingSnapshot snapshot)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(snapshot);
|
|
|
|
_currentBindings.Clear();
|
|
foreach (var action in snapshot.Actions)
|
|
{
|
|
_currentBindings[action.ActionName] = [..action.Bindings];
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void SetPrimaryBinding(string actionName, InputBindingDescriptor binding, bool swapIfTaken = true)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(actionName);
|
|
ArgumentNullException.ThrowIfNull(binding);
|
|
|
|
var targetBindings = GetOrCreateBindings(actionName);
|
|
var existingOwner = FindOwner(actionName, binding);
|
|
|
|
if (existingOwner is not null)
|
|
{
|
|
if (!swapIfTaken)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var previousPrimary = targetBindings.Count > 0 ? targetBindings[0] : null;
|
|
var ownerBindings = GetOrCreateBindings(existingOwner);
|
|
ReplaceBinding(ownerBindings, binding, previousPrimary);
|
|
}
|
|
|
|
RemoveBinding(targetBindings, binding);
|
|
targetBindings.Insert(0, binding);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void ResetAction(string actionName)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(actionName);
|
|
|
|
if (_defaultBindings.TryGetValue(actionName, out var bindings))
|
|
{
|
|
_currentBindings[actionName] = [..bindings];
|
|
return;
|
|
}
|
|
|
|
_currentBindings.Remove(actionName);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void ResetAll()
|
|
{
|
|
_currentBindings.Clear();
|
|
foreach (var pair in _defaultBindings)
|
|
{
|
|
_currentBindings[pair.Key] = [..pair.Value];
|
|
}
|
|
}
|
|
|
|
private static Dictionary<string, List<InputBindingDescriptor>> ToDictionary(InputBindingSnapshot snapshot)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(snapshot);
|
|
|
|
return snapshot.Actions.ToDictionary(
|
|
static action => action.ActionName,
|
|
static action => action.Bindings.ToList(),
|
|
StringComparer.Ordinal);
|
|
}
|
|
|
|
private static Dictionary<string, List<InputBindingDescriptor>> CloneDictionary(
|
|
IReadOnlyDictionary<string, List<InputBindingDescriptor>> source)
|
|
{
|
|
return source.ToDictionary(
|
|
static pair => pair.Key,
|
|
static pair => pair.Value.ToList(),
|
|
StringComparer.Ordinal);
|
|
}
|
|
|
|
private static void RemoveBinding(List<InputBindingDescriptor> bindings, InputBindingDescriptor binding)
|
|
{
|
|
bindings.RemoveAll(existing => AreEquivalent(existing, binding));
|
|
}
|
|
|
|
private static void ReplaceBinding(
|
|
List<InputBindingDescriptor> bindings,
|
|
InputBindingDescriptor bindingToReplace,
|
|
InputBindingDescriptor? replacement)
|
|
{
|
|
var index = bindings.FindIndex(existing => AreEquivalent(existing, bindingToReplace));
|
|
if (index < 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bindings.RemoveAt(index);
|
|
if (replacement is not null)
|
|
{
|
|
bindings.Insert(index, replacement);
|
|
}
|
|
}
|
|
|
|
private static bool AreEquivalent(InputBindingDescriptor left, InputBindingDescriptor right)
|
|
{
|
|
return left.DeviceKind == right.DeviceKind
|
|
&& left.BindingKind == right.BindingKind
|
|
&& string.Equals(left.Code, right.Code, StringComparison.Ordinal)
|
|
&& Nullable.Equals(left.AxisDirection, right.AxisDirection);
|
|
}
|
|
|
|
private List<InputBindingDescriptor> GetOrCreateBindings(string actionName)
|
|
{
|
|
if (!_currentBindings.TryGetValue(actionName, out var bindings))
|
|
{
|
|
bindings = [];
|
|
_currentBindings[actionName] = bindings;
|
|
}
|
|
|
|
return bindings;
|
|
}
|
|
|
|
private string? FindOwner(string excludedActionName, InputBindingDescriptor binding)
|
|
{
|
|
foreach (var pair in _currentBindings)
|
|
{
|
|
if (string.Equals(pair.Key, excludedActionName, StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (pair.Value.Any(existing => AreEquivalent(existing, binding)))
|
|
{
|
|
return pair.Key;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|