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 验证结果
317 lines
11 KiB
C#
317 lines
11 KiB
C#
// Copyright (c) 2025-2026 GeWuYou
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
using GFramework.Game.Abstractions.Input;
|
|
using GFramework.Godot.Input;
|
|
|
|
namespace GFramework.Godot.Tests.Input;
|
|
|
|
/// <summary>
|
|
/// 验证 Godot 输入绑定存储在纯托管后端上的动作快照、导入与冲突交换语义。
|
|
/// </summary>
|
|
[TestFixture]
|
|
public sealed class GodotInputBindingStoreTests
|
|
{
|
|
/// <summary>
|
|
/// 验证导出快照会反映后端提供的框架绑定描述。
|
|
/// </summary>
|
|
[Test]
|
|
public void ExportSnapshot_Should_ReturnBackendBindings()
|
|
{
|
|
var backend = new FakeInputMapBackend(
|
|
new InputBindingSnapshot(
|
|
[
|
|
new InputActionBinding(
|
|
"ui_accept",
|
|
[
|
|
new InputBindingDescriptor(
|
|
InputDeviceKind.KeyboardMouse,
|
|
InputBindingKind.Key,
|
|
"key:13",
|
|
"Enter")
|
|
])
|
|
]));
|
|
|
|
var store = new GodotInputBindingStore(backend);
|
|
var snapshot = store.ExportSnapshot();
|
|
var acceptBindings = snapshot.Actions.Single(
|
|
action => string.Equals(action.ActionName, "ui_accept", StringComparison.Ordinal));
|
|
|
|
Assert.That(acceptBindings.Bindings[0].Code, Is.EqualTo("key:13"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证导入快照后会把新绑定回写到后端,并能重新导出。
|
|
/// </summary>
|
|
[Test]
|
|
public void ImportSnapshot_Should_UpdateBackendBindings()
|
|
{
|
|
var backend = new FakeInputMapBackend(
|
|
new InputBindingSnapshot(
|
|
[
|
|
new InputActionBinding(
|
|
"ui_accept",
|
|
[
|
|
new InputBindingDescriptor(
|
|
InputDeviceKind.KeyboardMouse,
|
|
InputBindingKind.Key,
|
|
"key:13",
|
|
"Enter")
|
|
])
|
|
]));
|
|
|
|
var store = new GodotInputBindingStore(backend);
|
|
store.ImportSnapshot(
|
|
new InputBindingSnapshot(
|
|
[
|
|
new InputActionBinding(
|
|
"ui_accept",
|
|
[
|
|
new InputBindingDescriptor(
|
|
InputDeviceKind.KeyboardMouse,
|
|
InputBindingKind.Key,
|
|
"key:32",
|
|
"Space")
|
|
])
|
|
]));
|
|
|
|
var snapshot = store.ExportSnapshot();
|
|
var acceptBindings = snapshot.Actions.Single(
|
|
action => string.Equals(action.ActionName, "ui_accept", StringComparison.Ordinal));
|
|
|
|
Assert.That(acceptBindings.Bindings[0].Code, Is.EqualTo("key:32"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证导入快照时,会清空快照中未出现动作的后端绑定。
|
|
/// </summary>
|
|
[Test]
|
|
public void ImportSnapshot_WhenActionMissingFromSnapshot_Should_ClearBackendBindings()
|
|
{
|
|
var backend = new FakeInputMapBackend(
|
|
new InputBindingSnapshot(
|
|
[
|
|
new InputActionBinding(
|
|
"ui_accept",
|
|
[
|
|
new InputBindingDescriptor(
|
|
InputDeviceKind.KeyboardMouse,
|
|
InputBindingKind.Key,
|
|
"key:13",
|
|
"Enter")
|
|
]),
|
|
new InputActionBinding(
|
|
"ui_cancel",
|
|
[
|
|
new InputBindingDescriptor(
|
|
InputDeviceKind.KeyboardMouse,
|
|
InputBindingKind.Key,
|
|
"key:27",
|
|
"Escape")
|
|
])
|
|
]));
|
|
|
|
var store = new GodotInputBindingStore(backend);
|
|
store.ImportSnapshot(
|
|
new InputBindingSnapshot(
|
|
[
|
|
new InputActionBinding(
|
|
"ui_accept",
|
|
[
|
|
new InputBindingDescriptor(
|
|
InputDeviceKind.KeyboardMouse,
|
|
InputBindingKind.Key,
|
|
"key:32",
|
|
"Space")
|
|
])
|
|
]));
|
|
|
|
var snapshot = store.ExportSnapshot();
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(
|
|
snapshot.Actions.Single(action => string.Equals(action.ActionName, "ui_accept", StringComparison.Ordinal)).Bindings[0].Code,
|
|
Is.EqualTo("key:32"));
|
|
Assert.That(
|
|
snapshot.Actions.Single(action => string.Equals(action.ActionName, "ui_cancel", StringComparison.Ordinal)).Bindings,
|
|
Is.Empty);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证从纯托管绑定设置主绑定时,会保留 `Game` 层冲突交换语义。
|
|
/// </summary>
|
|
[Test]
|
|
public void SetPrimaryBinding_WhenBindingTaken_SwapsBackendBindings()
|
|
{
|
|
var backend = new FakeInputMapBackend(
|
|
new InputBindingSnapshot(
|
|
[
|
|
new InputActionBinding(
|
|
"move_left",
|
|
[
|
|
new InputBindingDescriptor(
|
|
InputDeviceKind.KeyboardMouse,
|
|
InputBindingKind.Key,
|
|
"key:65",
|
|
"A")
|
|
]),
|
|
new InputActionBinding(
|
|
"move_right",
|
|
[
|
|
new InputBindingDescriptor(
|
|
InputDeviceKind.KeyboardMouse,
|
|
InputBindingKind.Key,
|
|
"key:68",
|
|
"D")
|
|
])
|
|
]));
|
|
|
|
var store = new GodotInputBindingStore(backend);
|
|
store.SetPrimaryBinding(
|
|
"move_left",
|
|
new InputBindingDescriptor(
|
|
InputDeviceKind.KeyboardMouse,
|
|
InputBindingKind.Key,
|
|
"key:68",
|
|
"D"));
|
|
|
|
var snapshot = store.ExportSnapshot();
|
|
var moveLeft = snapshot.Actions.Single(
|
|
action => string.Equals(action.ActionName, "move_left", StringComparison.Ordinal));
|
|
var moveRight = snapshot.Actions.Single(
|
|
action => string.Equals(action.ActionName, "move_right", StringComparison.Ordinal));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(moveLeft.Bindings[0].Code, Is.EqualTo("key:68"));
|
|
Assert.That(moveRight.Bindings[0].Code, Is.EqualTo("key:65"));
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证重置全部绑定时,会移除运行时新增且默认快照中不存在的动作。
|
|
/// </summary>
|
|
[Test]
|
|
public void ResetAll_WhenRuntimeActionIsNotInDefaults_Should_RemoveAction()
|
|
{
|
|
var backend = new FakeInputMapBackend(
|
|
new InputBindingSnapshot(
|
|
[
|
|
new InputActionBinding(
|
|
"ui_accept",
|
|
[
|
|
new InputBindingDescriptor(
|
|
InputDeviceKind.KeyboardMouse,
|
|
InputBindingKind.Key,
|
|
"key:13",
|
|
"Enter")
|
|
])
|
|
]));
|
|
|
|
var store = new GodotInputBindingStore(backend);
|
|
store.ImportSnapshot(
|
|
new InputBindingSnapshot(
|
|
[
|
|
new InputActionBinding(
|
|
"ui_accept",
|
|
[
|
|
new InputBindingDescriptor(
|
|
InputDeviceKind.KeyboardMouse,
|
|
InputBindingKind.Key,
|
|
"key:13",
|
|
"Enter")
|
|
]),
|
|
new InputActionBinding(
|
|
"debug_toggle",
|
|
[
|
|
new InputBindingDescriptor(
|
|
InputDeviceKind.KeyboardMouse,
|
|
InputBindingKind.Key,
|
|
"key:192",
|
|
"QuoteLeft")
|
|
])
|
|
]));
|
|
|
|
store.ResetAll();
|
|
|
|
var snapshot = store.ExportSnapshot();
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(
|
|
snapshot.Actions.Any(action => string.Equals(action.ActionName, "ui_accept", StringComparison.Ordinal)),
|
|
Is.True);
|
|
Assert.That(
|
|
snapshot.Actions.Any(action => string.Equals(action.ActionName, "debug_toggle", StringComparison.Ordinal)),
|
|
Is.False);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 测试用的纯托管 InputMap 后端。
|
|
/// </summary>
|
|
private sealed class FakeInputMapBackend : IGodotInputMapBackend
|
|
{
|
|
private readonly Dictionary<string, List<InputBindingDescriptor>> _defaults;
|
|
private readonly Dictionary<string, List<InputBindingDescriptor>> _current;
|
|
|
|
/// <summary>
|
|
/// 初始化测试后端。
|
|
/// </summary>
|
|
/// <param name="snapshot">初始快照。</param>
|
|
public FakeInputMapBackend(InputBindingSnapshot snapshot)
|
|
{
|
|
_defaults = snapshot.Actions.ToDictionary(
|
|
static action => action.ActionName,
|
|
static action => action.Bindings.ToList(),
|
|
StringComparer.Ordinal);
|
|
_current = snapshot.Actions.ToDictionary(
|
|
static action => action.ActionName,
|
|
static action => action.Bindings.ToList(),
|
|
StringComparer.Ordinal);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<string> GetActionNames()
|
|
{
|
|
return [.._current.Keys.OrderBy(static key => key, StringComparer.Ordinal)];
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<InputBindingDescriptor> GetBindings(string actionName)
|
|
{
|
|
return _current.TryGetValue(actionName, out var bindings) ? [..bindings] : Array.Empty<InputBindingDescriptor>();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void SetBindings(string actionName, IReadOnlyList<InputBindingDescriptor> bindings)
|
|
{
|
|
_current[actionName] = [..bindings];
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void ResetAction(string actionName)
|
|
{
|
|
if (_defaults.TryGetValue(actionName, out var bindings))
|
|
{
|
|
_current[actionName] = [..bindings];
|
|
return;
|
|
}
|
|
|
|
_current.Remove(actionName);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void ResetAll()
|
|
{
|
|
_current.Clear();
|
|
foreach (var pair in _defaults)
|
|
{
|
|
_current[pair.Key] = [..pair.Value];
|
|
}
|
|
}
|
|
}
|
|
}
|