Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
code:
- 'src/**'
- 'tests/**'
- 'tools/**'
- '.github/workflows/**'
- 'global.json'
- '*.sln'
Expand Down Expand Up @@ -89,6 +90,48 @@ jobs:
**/*.csproj
.config/dotnet-tools.json

- name: 安裝 MSBuild(Native Shim)
uses: microsoft/setup-msbuild@v3

- name: 還原主專案 NuGet 套件(Native Shim)
run: dotnet restore ./src/InputBox/InputBox.csproj

- name: 讀取 Microsoft.GameInput 版本(Native Shim)
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
[xml]$project = Get-Content -LiteralPath "./src/InputBox/InputBox.csproj" -Raw
$packageRefs = @($project.Project.ItemGroup.PackageReference | Where-Object { $_.Include -eq 'Microsoft.GameInput' })

if ($packageRefs.Count -ne 1) {
Write-Error "InputBox.csproj 必須剛好包含一個 Microsoft.GameInput PackageReference;目前找到 $($packageRefs.Count) 個。"
}

$version = [string]$packageRefs[0].Version
if ([string]::IsNullOrWhiteSpace($version) -or $version -notmatch '^\d+\.\d+\.\d+(?:\.\d+)?$') {
Write-Error "Microsoft.GameInput Version 必須是明確穩定版本:$version"
}

"GAMEINPUT_VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
Write-Host "Microsoft.GameInput version: $version"

- name: 建置 GameInput Native Shim
shell: pwsh
run: |
msbuild .\src\InputBox.GameInput.Native\InputBox.GameInput.Native.vcxproj `
/p:Configuration=Release `
/p:Platform=x64 `
/p:PlatformToolset=v143 `
/p:GameInputPackageVersion=$env:GAMEINPUT_VERSION `
/m

- name: 驗證 GameInput Native Shim exports / probe / lifecycle
shell: pwsh
run: |
.\tools\Validate-GameInputNativeShim.ps1 `
-NativeShimPath ".\src\InputBox.GameInput.Native\bin\x64\Release\InputBox.GameInput.Native.dll" `
-ManagedSourcePath ".\src\InputBox\Core\Input\GameInputNative.cs"

- name: 建置應用程式與測試專案(Build App + Tests)
working-directory: ./tests/InputBox.Tests
# 測試專案會連同主專案相依一起建置,避免重複編譯浪費資源。
Expand Down Expand Up @@ -220,4 +263,4 @@ jobs:
name: ui-smoke-artifacts
path: TestResults/UiArtifacts
if-no-files-found: ignore
retention-days: 5
retention-days: 5
261 changes: 195 additions & 66 deletions .github/workflows/release.yml

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion InputBox.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<File Path="LICENSE" />
<File Path="README.md" />
</Folder>
<Project Path="src/InputBox.GameInput.Native/InputBox.GameInput.Native.vcxproj" Id="3e85c875-5e9d-4c7d-9b87-0d5b3eb3da53" />
<Project Path="src/InputBox/InputBox.csproj" />
<Project Path="tests/InputBox.Tests/InputBox.Tests.csproj" />
</Solution>
</Solution>
11 changes: 3 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@
- **XInput**:系統原生支援,無需額外安裝。
- **GameInput**:系統需具備 **GameInput 執行階段**。
- 多數 Windows 11 系統已內建 GameInput 執行階段;若系統未提供或載入失敗,本應用程式會自動退避至 **XInput** 相容模式。
- **可轉散發元件(選用)**:可透過微軟 [Microsoft.GameInput](https://www.nuget.org/packages/Microsoft.GameInput) NuGet 套件取得。
- 安裝方式:執行套件 `redist` 目錄下的 `GameInputRedist.msi`。
- **可轉散發元件(選用)**:正式發佈 ZIP 會隨附微軟 [Microsoft.GameInput](https://www.nuget.org/packages/Microsoft.GameInput) 套件提供的 `redist/GameInputRedist.msi`,供需要者手動安裝;本應用程式不會自動執行該安裝程式。

## Steam Deck/SteamOS 3 使用說明 🎮

Expand Down Expand Up @@ -590,10 +589,6 @@ Remove-Item Env:INPUTBOX_RUN_UI_TESTS -ErrorAction SilentlyContinue

- [.NET Runtime](https://github.com/dotnet/runtime):由 [Microsoft](https://github.com/microsoft) 及其 [貢獻者](https://github.com/dotnet/runtime/graphs/contributors) 開發並採用 [**MIT License**](https://github.com/dotnet/runtime/blob/main/LICENSE.TXT) 授權,作為本應用程式之底層執行環境,相關第三方聲明請參閱 [**THIRD-PARTY-NOTICES**](https://github.com/dotnet/runtime/blob/main/THIRD-PARTY-NOTICES.TXT)。
- [Windows Forms(WinForms)](https://github.com/dotnet/winforms):由 [Microsoft](https://github.com/microsoft) 及其 [貢獻者](https://github.com/dotnet/winforms/graphs/contributors) 開發並採用 [**MIT License**](https://github.com/dotnet/winforms/blob/main/LICENSE.TXT) 授權,提供桌面視窗圖形介面基礎架構,相關第三方聲明請參閱 [**THIRD-PARTY-NOTICES**](https://github.com/dotnet/winforms/blob/main/THIRD-PARTY-NOTICES.TXT)。
- [GameInput.Net](https://github.com/Cephy314/GameInputNet):由 [Cephy314](https://github.com/Cephy314) 及其[貢獻者](https://github.com/Cephy314/GameInputNet/graphs/contributors)開發並採用 [**MIT License**](https://github.com/Cephy314/GameInputNet/blob/main/LICENSE) 授權,用於存取 GameInput API。
- [UsbVendorsLibrary](https://github.com/Cephy314/UsbVendorsLibrary):由 [Cephy314](https://github.com/Cephy314) 及其[貢獻者](https://github.com/Cephy314/UsbVendorsLibrary/graphs/contributors)開發並採用 [**MIT License**](https://github.com/Cephy314/UsbVendorsLibrary/blob/main/LICENSE) 授權,用於存取裝置資訊。
- [USB ID Database(usb.ids)](http://www.linux-usb.org/usb-ids.html):由 Stephen J. Gowdy 等人維護的公開 USB 裝置識別碼資料庫。該資料庫採雙重授權([GNU General Public License(第 2 版或是更新的版本)](https://opensource.org/license/GPL-2.0)/[**3-clause BSD License**](https://opensource.org/license/bsd-3-clause))。
- 本應用程式透過第三方函式庫 [**UsbVendorsLibrary**](https://github.com/Cephy314/UsbVendorsLibrary) 使用該資料庫,並於 **自包含部署(self‑contained)** 發佈之執行檔中以嵌入資源形式包含其內容。
- 本專案依照官方雙重授權所明示之選項,以 [**3-clause BSD License**](https://opensource.org/license/bsd-3-clause) 條款隨附發佈;歸因聲明請參閱發佈檔 **Licenses/usb_ids_LICENSE.txt**。
- [Microsoft GameInput Redistributable](https://www.nuget.org/packages/Microsoft.GameInput):由 Microsoft 提供,作為選用的 GameInput 執行階段可轉散發元件。正式發佈 ZIP 會隨附 `redist/GameInputRedist.msi` 供使用者手動安裝;本應用程式不會自動安裝,且該 redist 依 Microsoft.GameInput 授權條款散布,不屬於本專案 CC0 授權範圍。

本專案的詳細條款與免責聲明,請參閱隨附之 [**LICENSE**](LICENSE) 文件;發佈檔 `Licenses/` 資料夾內含 `ThirdPartyNotices.txt`(NuGet 套件授權聲明清單)及各元件完整授權文字
本專案的詳細條款與免責聲明,請參閱隨附之 [**LICENSE**](LICENSE) 文件;發佈檔 `Licenses/` 資料夾內含 `ThirdPartyNotices.txt`(NuGet 套件授權聲明清單)、Microsoft GameInput 授權/Notice,以及各元件完整授權文字
58 changes: 58 additions & 0 deletions docs/engineering/gameinput-hardware-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# GameInput 手動硬體驗證矩陣

本文件定義 GameInput 相關變更在正式發佈或高風險修改前的手動硬體檢查表。它補足 CI 無法穩定覆蓋的實體控制器、藍牙、Windows GameInput 執行階段與 redist 情境。

這不是日常 PR 必跑項目,也不是 CI 關卡。若只是修改文件、測試或不影響 GameInput 硬體行為的程式碼,不需要執行本矩陣。

## 執行時機

建議在下列情境執行:

- 修改 `InputBox.GameInput.Native` shim、GameInput 受控端 interop 或 ABI。
- 修改 GameInput 連線/斷線偵測、重新列舉、callback、輪詢或退避行為。
- 修改 GameInput rumble 或緊急停止流程。
- 升級 `Microsoft.GameInput` NuGet 套件。
- 正式發佈前,作為發佈冒煙驗證的人工補充。

## 非目標

- 不取代 `dotnet build`、`dotnet test`、native export 驗證或 probe 冒煙測試。
- 不要求每個 PR 都跑完整硬體矩陣。
- 不要求維護者購買或常備所有控制器。
- 不新增 keyboard、mouse、sensors、raw report、force feedback、aggregate device 或 1:1 GameInput wrapper 的驗證範圍。

## 驗證前置條件

- 使用 Windows 環境與本機 Debug 或 Release 組建。
- 在 InputBox 設定中手動選擇 GameInput 提供者;預設提供者仍維持 XInput。
- 保留輸出視窗、`InputBox.log` 或測試者可取得的診斷輸出,方便記錄 GameInput shim 初始化、裝置狀態與退避訊息。
- 若某項硬體或環境不可取得,結果標記為 `略過`,並記錄原因。

## 驗證矩陣

| 項目 | 硬體 / 環境 | 操作步驟 | 預期結果 | 發佈必跑 | 可接受缺測 |
|---|---|---|---|---|---|
| Xbox USB | Xbox One / Xbox Series 控制器,以 USB 連線 | 啟動 InputBox,確認 GameInput 已連線;拔除並重接 3 次 | A11y 連線 / 斷線公告與實際狀態一致;不會卡在 connected;重新連線後仍可輸入 | 是 | 若沒有 Xbox 控制器,需以其他 XInput 相容 USB 控制器替代並記錄 |
| Xbox Bluetooth | Xbox One / Xbox Series 控制器,以藍牙連線 | 配對後啟動 InputBox;關閉控制器;重新喚醒或重新連線 | 裝置清單會重新整理;不殘留舊裝置;重新連線後不需重啟應用程式 | 是 | 若測試機沒有藍牙或控制器不支援藍牙,可標記略過 |
| Sony / DualSense | DualSense 或可回報 Sony VID/PID 的 PlayStation 控制器 | 連線後切換 Face 鍵 Auto 模式,確認 GameInput 診斷資訊中的 VID/PID 與配置 | Auto Face Button 解析為 PlayStation 配置;不依賴 USB database 或顯示名稱關鍵字才成功 | 建議 | 若沒有 Sony 控制器,可略過;正式發佈需記錄缺測 |
| Nintendo / Switch Pro | Switch Pro 或可回報 Nintendo VID/PID 的控制器 | 連線後切換 Face 鍵 Auto 模式,確認 GameInput 診斷資訊中的 VID/PID 與配置 | Auto Face Button 解析為 Nintendo 配置;不依賴 USB database 或顯示名稱關鍵字才成功 | 建議 | 若沒有 Nintendo 控制器,可略過;正式發佈需記錄缺測 |
| Elite / Paddle | Xbox Elite 或其他具 paddle / extra controls 的控制器 | 連線後檢查 GameInput capabilities / diagnostics | extra button / axis metadata 可進入診斷;paddle 或 extra controls 不觸發任何新的 InputBox 指令 | 建議 | 若沒有此類控制器,可略過 |
| 執行階段缺失 / Shim 載入失敗 | 未安裝 GameInput redist 的環境,或暫時遮蔽 native shim 的測試環境 | 啟動 InputBox 並手動選擇 GameInput 提供者 | GameInput 初始化失敗會公告並退避 XInput;沒有 crash;probe / log 能指出載入或初始化失敗原因 | 是 | 若測試機已有系統 GameInput 且無法安全遮蔽,需記錄未測原因 |
| Release ZIP | Release workflow 產物或本機發佈試跑 ZIP | 解壓縮並檢查 ZIP 結構,啟動 `InputBox.exe` 做 GameInput 冒煙驗證 | ZIP 不包含 `gameinput.dll` 或可見 `InputBox.GameInput.Native.dll` sidecar;包含 `redist/GameInputRedist.msi` 與第三方授權聲明 | 是 | 不可缺測 |

## 結果紀錄格式

建議在發佈說明、PR 描述或內部測試紀錄中使用下列格式:

```text
GameInput hardware verification:
- Xbox USB: 通過 (Xbox Series Controller, USB-C)
- Xbox Bluetooth: 通過 (Xbox Series Controller, Bluetooth)
- Sony / DualSense: 略過 (無可用硬體)
- Nintendo / Switch Pro: 略過 (無可用硬體)
- Elite / Paddle: 略過 (無可用硬體)
- 執行階段缺失 / Shim 載入失敗: 通過 (退避 XInput)
- Release ZIP: 通過
```

若任一發佈必跑項目失敗,應先修正或明確延後發佈;若標記略過,必須說明原因與替代驗證。
22 changes: 17 additions & 5 deletions docs/engineering/gamepad-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@
## 2. API 選擇與退避機制 (Provider & Backoff)
- **預設提供者**:應用程式之預設控制器提供者應設定為相容性最高之 **XInput**。
- **自動退避**:當使用者手動設定使用 `GameInput` 但初始化失敗(如系統不支援)時,系統必須**自動退避至 XInput** 並透過 `AnnounceA11y` 告知使用者。
- **GameInput 實作邊界**:GameInput 必須透過專案自有 `InputBox.GameInput.Native` shim 與專案內部 C# 型別存取 Microsoft GameInput runtime;不得重新引入已封存的第三方 `GameInput.Net`、`GameInputDotNet`、`UsbVendorsLibrary` 或 `usb.ids` 流程。
- **GameInput shim 同版發布**:Native shim 與 managed GameInput layer 視為同版、同包、一起發布的內部實作;更新 C ABI 時直接同步兩端 struct,不維護外部相容層或 `V2` suffix。若 shim 載入失敗或 ABI 不符,應走既有 GameInput 初始化失敗並退避 XInput 路徑。
- **GameInput shim ABI 防呆**:shim 必須在 `InputBoxGameInputGetShimInfo` 回報 native ABI number、`GAMEINPUT_API_VERSION`、pointer size 與所有跨邊界 struct size;managed layer 必須以 `Marshal.SizeOf<T>()` 驗證尺寸,不符即視為 shim 載錯並退避 XInput。
- **GameInput shim 發佈驗證**:CI 與 release 必須在 native shim 建置後比對 `GameInputNativeMethods` 宣告的 `InputBoxGameInput*` EntryPoint 與 DLL exports,並執行 `InputBoxGameInputProbeRuntime` smoke test 與 native lifecycle stress smoke;缺 export、probe crash 或 native/managed struct size 不符時不得產生發佈 ZIP。若 GameInput runtime 不可用,lifecycle stress 可略過,但 export/probe 守門不可略過。CI 的 code filter 必須包含 `tools/**`,避免驗證腳本異動未觸發守門。
- **GameInput 手動硬體驗證**:自動驗證通過後,若正式發佈或變更內容涉及 GameInput 硬體行為,仍應依 `docs/engineering/gameinput-hardware-verification.md` 抽測實體控制器。此矩陣不是 CI 關卡,也不是每個 PR 的必跑項目。
- **GameInput runtime probe**:shim 必須提供建立 context 前可呼叫的 runtime probe,回報 LoadLibrary、GetProcAddress、GameInputInitialize 的 HRESULT / Win32 error、嘗試與實際載入的 module kind/path,以及字串截斷旗標;此資訊只供 log 與測試,不可影響按鍵語意。
- **GameInput DLL 載入安全**:系統 GameInput runtime 只能使用 System32 搜尋範圍;registry redist 絕對路徑必須搭配 `LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32`,並記錄實際載入來源。不得從工作目錄、目前目錄或未限定搜尋路徑載入 `gameinput.dll`。
- **GameInput native context 同步**:native shim 必須以 SRW lock 保護 `devices`、`callbacks`、診斷 counter,以及 refresh/read/unregister/destroy 等會交錯的路徑;callback 只能複製 POD event 或設定喚醒訊號,不得持有 context lock 呼叫 managed callback。
- **GameInput 診斷 metadata 邊界**:string truncation flags、timestamp stale counters、missing reading counters、device zombie refresh counters 只能進入 log、測試或未來診斷快照,不可直接改變 edge detection、Pause/Resume neutral gate 或任何 UI 命令。
- **GameInput subset 範圍**:目前只實作 InputBox 需要的 Gamepad subset:裝置資訊、VID/PID、timestamp、gamepad button state、rumble、device/reading callbacks,以及 gamepad capabilities / extra control metadata。不得在未重新評估安全與產品需求前擴張到 keyboard、mouse、sensors、raw report、force feedback、aggregate device 或 1:1 GameInput wrapper。
- **Callback 使用邊界**:GameInput 的 `RegisterDeviceCallback` / `RegisterReadingCallback` 僅可用於要求背景 MTA polling thread 重新整理或喚醒診斷路徑;正式輸入命令仍必須由 60 FPS polling 消費,callback 不得直接觸發 UI、剪貼簿、返回前景或任何輸入動作。
- **GameInput runtime/redist 政策**:`Microsoft.GameInput` NuGet 僅作為官方 SDK、header 與 redist 來源。發佈 ZIP 可隨附 `redist/GameInputRedist.msi` 供使用者手動安裝,但應用程式不得自動執行安裝程式,也不得要求一般啟動流程取得系統管理員權限。
- **Auto 模式解析優先序**:當使用者選擇 Face 鍵 `Auto` 模式且目前提供者為 **GameInput** 時,必須先使用較穩定的硬體識別資訊(例如 VID/PID、產品家族名稱)判斷裝置類型,再退回裝置名稱關鍵字比對;像 `Wireless`、`Bluetooth`、`Adapter`、`Receiver` 等連線詞應視為雜訊而忽略。
- **Auto 模式預設對照**:在上述自動判斷下,Sony / PlayStation 裝置預設應解析為 **PlayStation(× 確認)**,Nintendo 裝置解析為 Nintendo 配置,其餘則回到 Xbox 配置;若使用者手動指定模式,必須優先於自動判斷。
- **緊急停止**:程式結束前必須執行 `EmergencyStopAllActiveControllers()` 強制停止所有控制器馬達。
Expand Down Expand Up @@ -57,13 +69,13 @@ XInput 以 `short` 範圍 (±32767) 運作;GameInput 以 float `[-1.0, 1.0]`
- 裝置連線後立即以第一幀快照重複執行 **50 次** EMA,使 bias 在第一幀即收斂至約 99%,避免連線初期因 bias ≈ 0 造成方向誤判。
- 暖機時固定傳入 `isDPadActive: false`。

### 4.5 右向映射保護(各後端可採不同機制,效果須等效
### 4.5 方向映射統一規範(硬體平等原則

右向映射較易受到正偏噪聲影響(尤其筆電環境),各後端須各自防止以下場景:
「DPad-Right 方向觸發後,左搖桿殘餘偏移立即重觸發右向」
兩個後端 (XInput / GameInput) 必須統一採用 `GamepadDeadzoneHysteresis.ResolveDirection`,以對稱 `Enter / Exit` 閾值處理四個方向(左、右、上、下),**不得**為單一方向(例如右向)實作差別補償(如非對稱退出閾值、單向抑制旗標)。

- **XInput**:以 `_suppressMappedRightFromLeftStick` 旗標實作,重置方向狀態時若上一方向為 Right,則啟動抑制,直到左搖桿回到中立區。另配合非對稱退出閾值 `max(exit, enter × 0.75f)` 提高黏滯防護。
- **GameInput**:以 `ResetDirectionalRepeatState` 清除 `_previousProcessedButtons` 的 DPad 位元,強制下次觸發須重新穿越 Enter 閾值,配合硬體校準輸出達到等效保護。
- **與業界黃金標準對齊**:Steam Input、DS4Windows、Unity InputSystem 等主流方案均提供 per-stick 或 per-axis 對稱 deadzone,不採 per-direction 差別補償;本專案遵循相同原則,避免按鍵映射行為因「特定硬體」(例如筆電觸控板控制器的正偏噪聲)而產生不可預測的方向差異。
- **硬體漂移處方**:若特定硬體出現方向 sticky 或漂移現象,正確做法是請使用者(對稱地)提高 `ThumbDeadzoneEnter / ThumbDeadzoneExit` 設定值,並交由 §4.1–§4.4 的 EMA bias 學習機制與 §4.3 的 D-Pad 機械耦合閘門共同補償;**禁止**在按鍵映射程式碼路徑中為單一方向加入差別補償。
- **GameInput 架構等效保護**:GameInput 使用 `_previousProcessedButtons` 進行 D-Pad 邊緣偵測,因此 `ResetDirectionalRepeatState` 必須清除 `_previousProcessedButtons` 的 D-Pad 位元,強制下次觸發須重新穿越 `Enter` 閾值;這是 GameInput 邊緣偵測架構的需求,而非右向強化,與 XInput 透過 `previousState.Has(...)` 直接讀取硬體狀態的邊緣偵測方式相對應。

## 5. Dialog 層級控制器整合
- **ConnectionChanged 強制訂閱**:任何持有或使用 `IGamepadController` 的 Dialog(對話框),在 `BindGamepadEvents` 中**必須**同時訂閱 `ConnectionChanged`,並在 `UnbindGamepadEvents` 中解除。
Expand Down
Loading
Loading