mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-13 06:04:30 +08:00
fix(input): 修复输入绑定快照与导入语义
- 修复 InputBindingStore 只读查询会污染导出快照的问题 - 修复 Godot 输入绑定导入时未清理残留动作绑定的问题 - 补充输入运行时与 Godot backend 的 XML 契约说明和 README 入口 - 更新 ai-plan 跟踪并补充针对 PR #346 的回归测试
This commit is contained in:
parent
ebbef321ad
commit
5f9589ed3c
@ -281,6 +281,7 @@ public sealed class ContinueGameCommandHandler
|
|||||||
- 序列化系统:[序列化系统](../docs/zh-CN/game/serialization.md)
|
- 序列化系统:[序列化系统](../docs/zh-CN/game/serialization.md)
|
||||||
- 场景系统:[场景系统](../docs/zh-CN/game/scene.md)
|
- 场景系统:[场景系统](../docs/zh-CN/game/scene.md)
|
||||||
- UI 系统:[UI 系统](../docs/zh-CN/game/ui.md)
|
- UI 系统:[UI 系统](../docs/zh-CN/game/ui.md)
|
||||||
|
- 输入系统:[输入系统](../docs/zh-CN/game/input.md)
|
||||||
|
|
||||||
## 选择建议
|
## 选择建议
|
||||||
|
|
||||||
|
|||||||
@ -56,6 +56,25 @@ public sealed class InputBindingStoreTests
|
|||||||
Is.EqualTo("key:65"));
|
Is.EqualTo("key:65"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证查询不存在的动作时,不会把空条目写回当前快照。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void GetBindings_WhenActionMissing_Should_NotMutateSnapshot()
|
||||||
|
{
|
||||||
|
var store = CreateStore();
|
||||||
|
|
||||||
|
var missingBindings = store.GetBindings("jump");
|
||||||
|
var snapshot = store.ExportSnapshot();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(missingBindings.ActionName, Is.EqualTo("jump"));
|
||||||
|
Assert.That(missingBindings.Bindings, Is.Empty);
|
||||||
|
Assert.That(snapshot.Actions.Any(action => string.Equals(action.ActionName, "jump", StringComparison.Ordinal)), Is.False);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static InputBindingStore CreateStore()
|
private static InputBindingStore CreateStore()
|
||||||
{
|
{
|
||||||
return new InputBindingStore(
|
return new InputBindingStore(
|
||||||
|
|||||||
@ -30,8 +30,11 @@ public sealed class InputBindingStore : IInputBindingStore
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public InputActionBinding GetBindings(string actionName)
|
public InputActionBinding GetBindings(string actionName)
|
||||||
{
|
{
|
||||||
var bindings = GetOrCreateBindings(actionName);
|
ArgumentException.ThrowIfNullOrWhiteSpace(actionName);
|
||||||
return new InputActionBinding(actionName, bindings.ToArray());
|
|
||||||
|
return _currentBindings.TryGetValue(actionName, out var bindings)
|
||||||
|
? new InputActionBinding(actionName, bindings.ToArray())
|
||||||
|
: new InputActionBinding(actionName, Array.Empty<InputBindingDescriptor>());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@ -19,12 +19,21 @@ public sealed class InputDeviceTracker : IInputDeviceTracker
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
/// <remarks>
|
||||||
|
/// 该属性不提供额外同步原语。
|
||||||
|
/// 宿主应在同一输入线程内调用 <see cref="Update" /> 并读取当前值,例如 Godot 的主线程或输入事件线程。
|
||||||
|
/// </remarks>
|
||||||
public InputDeviceContext CurrentDevice { get; private set; }
|
public InputDeviceContext CurrentDevice { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 使用新的宿主设备上下文覆盖当前状态。
|
/// 使用新的宿主设备上下文覆盖当前状态。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">新的设备上下文。</param>
|
/// <param name="context">新的设备上下文。</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// 该方法设计给宿主输入线程串行调用。
|
||||||
|
/// 如果宿主需要跨线程读取设备上下文,应在外层提供自己的同步策略,而不是依赖此类型完成可见性保证。
|
||||||
|
/// </remarks>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="context" /> 为 <see langword="null" /> 时抛出。</exception>
|
||||||
public void Update(InputDeviceContext context)
|
public void Update(InputDeviceContext context)
|
||||||
{
|
{
|
||||||
CurrentDevice = context ?? throw new ArgumentNullException(nameof(context));
|
CurrentDevice = context ?? throw new ArgumentNullException(nameof(context));
|
||||||
|
|||||||
@ -19,6 +19,8 @@ public sealed class UiInputDispatcher : IUiInputDispatcher
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="actionMap">动作映射表。</param>
|
/// <param name="actionMap">动作映射表。</param>
|
||||||
/// <param name="router">目标 UI 路由器。</param>
|
/// <param name="router">目标 UI 路由器。</param>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="actionMap" /> 为 <see langword="null" /> 时抛出。</exception>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="router" /> 为 <see langword="null" /> 时抛出。</exception>
|
||||||
public UiInputDispatcher(IUiInputActionMap actionMap, IUiRouter router)
|
public UiInputDispatcher(IUiInputActionMap actionMap, IUiRouter router)
|
||||||
{
|
{
|
||||||
_actionMap = actionMap ?? throw new ArgumentNullException(nameof(actionMap));
|
_actionMap = actionMap ?? throw new ArgumentNullException(nameof(actionMap));
|
||||||
|
|||||||
@ -372,6 +372,7 @@ public sealed class MyUiRouter : UiRouterBase
|
|||||||
- 序列化系统:[序列化系统](../docs/zh-CN/game/serialization.md)
|
- 序列化系统:[序列化系统](../docs/zh-CN/game/serialization.md)
|
||||||
- 场景系统:[场景系统](../docs/zh-CN/game/scene.md)
|
- 场景系统:[场景系统](../docs/zh-CN/game/scene.md)
|
||||||
- UI 系统:[UI 系统](../docs/zh-CN/game/ui.md)
|
- UI 系统:[UI 系统](../docs/zh-CN/game/ui.md)
|
||||||
|
- 输入系统:[输入系统](../docs/zh-CN/game/input.md)
|
||||||
|
|
||||||
## 什么时候不该直接依赖本包
|
## 什么时候不该直接依赖本包
|
||||||
|
|
||||||
|
|||||||
@ -82,6 +82,63 @@ public sealed class GodotInputBindingStoreTests
|
|||||||
Assert.That(acceptBindings.Bindings[0].Code, Is.EqualTo("key:32"));
|
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>
|
/// <summary>
|
||||||
/// 验证从纯托管绑定设置主绑定时,会保留 `Game` 层冲突交换语义。
|
/// 验证从纯托管绑定设置主绑定时,会保留 `Game` 层冲突交换语义。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -61,10 +61,25 @@ public sealed class GodotInputBindingStore : IInputBindingStore, IInputDeviceTra
|
|||||||
ArgumentNullException.ThrowIfNull(snapshot);
|
ArgumentNullException.ThrowIfNull(snapshot);
|
||||||
|
|
||||||
ReloadFromBackend();
|
ReloadFromBackend();
|
||||||
|
var snapshotActionNames = snapshot.Actions
|
||||||
|
.Select(static action => action.ActionName)
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
var removedActionNames = _state.ExportSnapshot().Actions
|
||||||
|
.Select(static action => action.ActionName)
|
||||||
|
.Where(actionName => !snapshotActionNames.Contains(actionName))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var actionName in removedActionNames)
|
||||||
|
{
|
||||||
|
_backend.SetBindings(actionName, Array.Empty<InputBindingDescriptor>());
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var action in snapshot.Actions)
|
foreach (var action in snapshot.Actions)
|
||||||
{
|
{
|
||||||
ApplyActionBindings(action);
|
ApplyActionBindings(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ReloadFromBackend();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@ -32,6 +32,13 @@ internal sealed class GodotInputMapBackend : IGodotInputMapBackend
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyList<InputBindingDescriptor> GetBindings(string actionName)
|
public IReadOnlyList<InputBindingDescriptor> GetBindings(string actionName)
|
||||||
{
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(actionName);
|
||||||
|
|
||||||
|
if (!InputMap.HasAction(actionName))
|
||||||
|
{
|
||||||
|
return Array.Empty<InputBindingDescriptor>();
|
||||||
|
}
|
||||||
|
|
||||||
var bindings = new List<InputBindingDescriptor>();
|
var bindings = new List<InputBindingDescriptor>();
|
||||||
foreach (var inputEvent in InputMap.ActionGetEvents(actionName))
|
foreach (var inputEvent in InputMap.ActionGetEvents(actionName))
|
||||||
{
|
{
|
||||||
@ -73,7 +80,10 @@ internal sealed class GodotInputMapBackend : IGodotInputMapBackend
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
InputMap.ActionEraseEvents(actionName);
|
if (InputMap.HasAction(actionName))
|
||||||
|
{
|
||||||
|
InputMap.ActionEraseEvents(actionName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@ -13,27 +13,31 @@ internal interface IGodotInputMapBackend
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取当前 `InputMap` 中的动作名。
|
/// 获取当前 `InputMap` 中的动作名。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>动作名列表。</returns>
|
/// <returns>动作名列表;永远不会返回 <see langword="null" />。</returns>
|
||||||
IReadOnlyList<string> GetActionNames();
|
IReadOnlyList<string> GetActionNames();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取指定动作的框架绑定描述集合。
|
/// 获取指定动作的框架绑定描述集合。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="actionName">动作名称。</param>
|
/// <param name="actionName">动作名称,不能为 <see langword="null" /> 或空白字符串。</param>
|
||||||
/// <returns>框架绑定描述集合。</returns>
|
/// <returns>框架绑定描述集合;永远不会返回 <see langword="null" />。</returns>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="actionName" /> 为 <see langword="null" /> 或空白字符串时抛出。</exception>
|
||||||
IReadOnlyList<InputBindingDescriptor> GetBindings(string actionName);
|
IReadOnlyList<InputBindingDescriptor> GetBindings(string actionName);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用给定绑定集合替换动作当前绑定。
|
/// 用给定绑定集合替换动作当前绑定。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="actionName">动作名称。</param>
|
/// <param name="actionName">动作名称,不能为 <see langword="null" /> 或空白字符串。</param>
|
||||||
/// <param name="bindings">新的绑定集合。</param>
|
/// <param name="bindings">新的绑定集合,不能为 <see langword="null" />,但可以为空集合。</param>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="actionName" /> 为 <see langword="null" /> 或空白字符串时抛出。</exception>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="bindings" /> 为 <see langword="null" /> 时抛出。</exception>
|
||||||
void SetBindings(string actionName, IReadOnlyList<InputBindingDescriptor> bindings);
|
void SetBindings(string actionName, IReadOnlyList<InputBindingDescriptor> bindings);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将指定动作恢复为项目默认绑定。
|
/// 将指定动作恢复为项目默认绑定。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="actionName">动作名称。</param>
|
/// <param name="actionName">动作名称,不能为 <see langword="null" /> 或空白字符串。</param>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="actionName" /> 为 <see langword="null" /> 或空白字符串时抛出。</exception>
|
||||||
void ResetAction(string actionName);
|
void ResetAction(string actionName);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -14,11 +14,14 @@ Godot `InputMap` 适配,优先服务 UI 语义动作桥接和绑定重映射
|
|||||||
- 已新增 `GFramework.Game.Input` 默认运行时,覆盖纯托管绑定存储、设备上下文持有者和逻辑动作到 `UiInputAction` 的桥接
|
- 已新增 `GFramework.Game.Input` 默认运行时,覆盖纯托管绑定存储、设备上下文持有者和逻辑动作到 `UiInputAction` 的桥接
|
||||||
- 已新增 `GFramework.Godot.Input` 适配层,覆盖 `InputMap` 绑定读写与 descriptor-based backend 桥接
|
- 已新增 `GFramework.Godot.Input` 适配层,覆盖 `InputMap` 绑定读写与 descriptor-based backend 桥接
|
||||||
- 已补 `Game.Tests` 与 `Godot.Tests` 的新增回归,并补 `docs/zh-CN/game/input.md` 与 `docs/zh-CN/godot/input.md`
|
- 已补 `Game.Tests` 与 `Godot.Tests` 的新增回归,并补 `docs/zh-CN/game/input.md` 与 `docs/zh-CN/godot/input.md`
|
||||||
|
- 已处理 PR `#346` 的首轮 review follow-up,修复只读查询污染快照与 Godot 导入快照残留绑定问题,并补齐 README / XML / `ai-plan` 收尾
|
||||||
|
|
||||||
## 当前状态摘要
|
## 当前状态摘要
|
||||||
|
|
||||||
- 统一输入抽象已建立,但当前仍聚焦动作绑定和 UI 输入桥接,不尝试覆盖完整 gameplay input runtime
|
- 统一输入抽象已建立,但当前仍聚焦动作绑定和 UI 输入桥接,不尝试覆盖完整 gameplay input runtime
|
||||||
- `GodotInputBindingStore` 当前把 `InputMap` 默认绑定和主绑定替换接到框架抽象,允许导出 / 导入 `InputBindingSnapshot`
|
- `GodotInputBindingStore` 当前把 `InputMap` 默认绑定和主绑定替换接到框架抽象,允许导出 / 导入 `InputBindingSnapshot`
|
||||||
|
- `InputBindingStore.GetBindings(...)` 已改为纯读取语义,不再因查询缺失动作而把空条目带进导出快照
|
||||||
|
- `GodotInputBindingStore.ImportSnapshot(...)` 已改为快照级覆盖语义,会清空快照中未出现动作的后端绑定
|
||||||
- `project.godot -> InputActions` 生成器链路保持不变,新的输入系统直接复用动作名常量,而不是替代它
|
- `project.godot -> InputActions` 生成器链路保持不变,新的输入系统直接复用动作名常量,而不是替代它
|
||||||
|
|
||||||
## 当前风险
|
## 当前风险
|
||||||
@ -40,9 +43,12 @@ Godot `InputMap` 适配,优先服务 UI 语义动作桥接和绑定重映射
|
|||||||
- 结果:通过
|
- 结果:通过
|
||||||
- `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter "FullyQualifiedName~GodotInputBindingStoreTests" -m:1 -p:RestoreFallbackFolders= -nodeReuse:false`
|
- `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter "FullyQualifiedName~GodotInputBindingStoreTests" -m:1 -p:RestoreFallbackFolders= -nodeReuse:false`
|
||||||
- 结果:通过
|
- 结果:通过
|
||||||
|
- `python3 scripts/license-header.py --check --paths GFramework.Game.Abstractions/README.md GFramework.Game.Tests/Input/InputBindingStoreTests.cs GFramework.Game/Input/InputBindingStore.cs GFramework.Game/Input/InputDeviceTracker.cs GFramework.Game/Input/UiInputDispatcher.cs GFramework.Game/README.md GFramework.Godot.Tests/Input/GodotInputBindingStoreTests.cs GFramework.Godot/Input/GodotInputBindingStore.cs GFramework.Godot/Input/GodotInputMapBackend.cs GFramework.Godot/Input/IGodotInputMapBackend.cs ai-plan/public/input-system-godot-integration/traces/input-system-godot-integration-trace.md`
|
||||||
|
- 结果:待本轮验证补录
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
1. 若继续扩展输入系统,优先补更多逻辑动作与 gameplay 输入场景,而不是先扩面到品牌图标、震动预设或平台文案
|
1. 若继续扩展输入系统,优先补更多逻辑动作与 gameplay 输入场景,而不是先扩面到品牌图标、震动预设或平台文案
|
||||||
2. 若要增强 Godot 宿主覆盖,优先补真实 `InputMap` / `InputEvent` 集成测试宿主,而不是把更多原生对象直接放进普通 `dotnet test`
|
2. 若要增强 Godot 宿主覆盖,优先补真实 `InputMap` / `InputEvent` 集成测试宿主,而不是把更多原生对象直接放进普通 `dotnet test`
|
||||||
3. 若要开放给消费者使用,继续完善 `README.md`、模块 README 与教程中的采用路径示例
|
3. 若要开放给消费者使用,继续完善 `README.md`、模块 README 与教程中的采用路径示例
|
||||||
|
4. 若继续处理 PR review,可再评估值对象改成 `record` 的收益与兼容性,而不是把该风格建议与行为修复混在同一波提交
|
||||||
|
|||||||
@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
### 阶段:统一输入抽象与 Godot 适配首轮落地(RP-001)
|
### 阶段:统一输入抽象与 Godot 适配首轮落地(RP-001)
|
||||||
|
|
||||||
- 创建长分支 `feat/input-system-godot-integration`,并在 `GFramework-WorkTree/GFramework-input-system-godot-integration`
|
- 创建长分支 `feat/input-system-godot-integration`,并在 `feat/input-system-godot-integration#346`
|
||||||
建立独立 worktree
|
上推进独立实现与验证
|
||||||
- 在 `GFramework.Game.Abstractions/Input/` 新增:
|
- 在 `GFramework.Game.Abstractions/Input/` 新增:
|
||||||
- `InputBindingDescriptor`
|
- `InputBindingDescriptor`
|
||||||
- `InputActionBinding`
|
- `InputActionBinding`
|
||||||
@ -48,3 +48,27 @@
|
|||||||
|
|
||||||
1. 若继续推进输入系统,优先定义更多逻辑动作与 gameplay 输入桥接,而不是先扩到宿主品牌文案
|
1. 若继续推进输入系统,优先定义更多逻辑动作与 gameplay 输入桥接,而不是先扩到宿主品牌文案
|
||||||
2. 若要增强 Godot 验证,单独准备真实 `InputMap` / `InputEvent` 集成宿主,而不是依赖普通 VSTest process
|
2. 若要增强 Godot 验证,单独准备真实 `InputMap` / `InputEvent` 集成宿主,而不是依赖普通 VSTest process
|
||||||
|
|
||||||
|
## 2026-05-11
|
||||||
|
|
||||||
|
### 阶段:PR #346 review follow-up(RP-001)
|
||||||
|
|
||||||
|
- 核对当前分支 PR `#346` 的 CodeRabbit review,确认以下问题仍然适用于本地代码:
|
||||||
|
- `InputBindingStore.GetBindings(...)` 读取缺失动作时会隐式创建空条目,并污染 `ExportSnapshot()`
|
||||||
|
- `GodotInputBindingStore.ImportSnapshot(...)` 只覆盖快照内动作,未清空后端残留绑定
|
||||||
|
- `InputDeviceTracker` / `UiInputDispatcher` / `IGodotInputMapBackend` 的 XML 文档缺少线程或异常契约
|
||||||
|
- `GFramework.Game.Abstractions/README.md` 与 `GFramework.Game/README.md` 缺少输入系统文档入口
|
||||||
|
- 公开 trace 中仍包含 worktree 目录名,已改为 `feat/input-system-godot-integration#346`
|
||||||
|
- 本轮未跟进 `InputBindingDescriptor`、`InputActionBinding`、`InputBindingSnapshot`、`InputDeviceContext` 改成 `record` 的 nitpick
|
||||||
|
- 原因:这些建议偏向值语义风格统一,不是当前 PR 中已验证的行为缺陷;本轮优先收敛真实回归风险与契约缺口
|
||||||
|
- 新增回归测试:
|
||||||
|
- `InputBindingStoreTests.GetBindings_WhenActionMissing_Should_NotMutateSnapshot`
|
||||||
|
- `GodotInputBindingStoreTests.ImportSnapshot_WhenActionMissingFromSnapshot_Should_ClearBackendBindings`
|
||||||
|
- 验证结果:
|
||||||
|
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~InputBindingStoreTests|FullyQualifiedName~UiInputDispatcherTests"` 通过
|
||||||
|
- `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter "FullyQualifiedName~GodotInputBindingStoreTests"` 通过
|
||||||
|
|
||||||
|
### 下一步
|
||||||
|
|
||||||
|
1. 运行针对本次改动文件的 license-header 检查并补录结果
|
||||||
|
2. 如需继续消化 PR review,再单独评估值对象切换到 `record` 是否值得放进同一个 PR
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user