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
2 changes: 2 additions & 0 deletions docs/engineering/core-engineering.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@
- **格式與診斷紀律**:
- **EditorConfig 強制套用**:每次修改任何檔案後,必須遵循專案根目錄 `.editorconfig` 的縮排、編碼、換行與格式設定。
- **C# 診斷清零**:每次修改 `*.cs` 檔案後,提交或回覆前都必須檢查該檔案的 IDE 與 CS 類型診斷,並修正新增的建議、警告與錯誤。
- **Release 日誌門檻**:正式版預設只寫入 `Warning` 以上;`Info` 僅供 Debug、測試主機或 `INPUTBOX_LOG_LEVEL=Info` 臨時診斷使用。
- **可行動診斷原則**:正常 lifecycle、成功 probe、成功或略過震動、一般輪詢健康資料應維持 `Info`;會影響使用者操作、裝置可用性、API 呼叫失敗或資料修復失敗的訊號才可升級為 `Warning` 或 `Error`。
1 change: 1 addition & 0 deletions docs/engineering/gamepad-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- **GameInput 發佈驗證**:CI 與 release 不再建置舊版 `InputBox.GameInput.Native` 原生專案;發佈輸出與 ZIP 必須確認不包含 `InputBox.GameInput.Native.dll` 或 `gameinput.dll` sidecar。GameInput runtime 不可用時,仍必須走既有初始化失敗並退避 XInput 路徑。
- **GameInput 手動硬體驗證**:自動驗證通過後,若正式發佈或變更內容涉及 GameInput 硬體行為,仍應依 `docs/engineering/gameinput-hardware-verification.md` 抽測實體控制器。此矩陣不是 CI 關卡,也不是每個 PR 的必跑項目。
- **GameInput runtime probe**:必須保留 InputWeave 的 runtime probe 診斷,回報 loader policy、HRESULT / Win32 error、候選與實際選取的 module kind/path/version;此資訊只供 log 與測試,不可影響按鍵語意。
- **GameInput Release 日誌邊界**:正常 `GameInputRuntime/InputWeave`、`GameInputDiag reason=init/stop-polling/dispose` 與 repeated timestamp 計數只屬於 `Info` 診斷;只有 runtime 不可用、callback 註冊失敗、missing reading 門檻、裝置狀態非 connected、讀取例外或震動 API 失敗才可在 Release 預設門檻下輸出。
- **GameInput DLL 載入安全**:GameInput runtime 載入策略由 InputWeave 負責,InputBox 不得自行從工作目錄、目前目錄或未限定搜尋路徑載入 `gameinput.dll`。
- **GameInput 同步**:InputBox 仍必須在背景 MTA polling thread 建立、存取與釋放 InputWeave `GameInputClient` / `GameInputDevice` / callback registration;callback 只能設定喚醒或重新整理訊號,不得直接觸發 managed UI 命令。
- **GameInput 診斷 metadata 邊界**:runtime probe、timestamp stale counters、missing reading counters、device unavailable refresh counters 只能進入 log、測試或未來診斷快照,不可直接改變 edge detection、Pause/Resume neutral gate 或任何 UI 命令。
Expand Down
58 changes: 36 additions & 22 deletions src/InputBox/Core/Input/GameInputGamepadController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -861,7 +861,7 @@ private void SetupReadingCallback()
}
catch (Exception ex)
{
LoggerService.LogException(ex, "GameInput 註冊 ReadingCallback 失敗");
LoggerService.LogWarning($"GameInput 註冊 ReadingCallback 失敗 exception={ex.GetType().Name} message={ex.Message}");

Debug.WriteLine($"GameInput 註冊 ReadingCallback 失敗:{ex.Message}");
}
Expand Down Expand Up @@ -940,8 +940,17 @@ private async Task PollingLoopAsync(CancellationToken cancellationToken, TaskCom
probeInfo.Candidates.Select(candidate =>
$"{candidate.ModuleKind}:exists={candidate.Exists}:loadHr=0x{unchecked((uint)candidate.LoadHResult):X8}:procHr=0x{unchecked((uint)candidate.GetProcAddressHResult):X8}:win32={candidate.Win32Error}:path={candidate.ModulePath}"));

LoggerService.LogInfo(
$"GameInputProbe/InputWeave available={probeInfo.IsAvailable} hr=0x{unchecked((uint)probeInfo.HResult):X8} win32={probeInfo.Win32Error} selectedKind={probeInfo.SelectedModuleKind} selectedPath={probeInfo.SelectedModulePath} selectedVersion={probeInfo.SelectedFileVersion} candidates={candidateSummary}");
string probeMessage =
$"GameInputProbe/InputWeave available={probeInfo.IsAvailable} hr=0x{unchecked((uint)probeInfo.HResult):X8} win32={probeInfo.Win32Error} selectedKind={probeInfo.SelectedModuleKind} selectedPath={probeInfo.SelectedModulePath} selectedVersion={probeInfo.SelectedFileVersion} candidates={candidateSummary}";

if (probeInfo.IsAvailable)
{
LoggerService.LogInfo(probeMessage);
}
else
{
LoggerService.LogWarning(probeMessage);
}
}

Debug.WriteLine($"GameInput 在背景執行緒初始化失敗:{ex.Message}");
Expand Down Expand Up @@ -1053,15 +1062,33 @@ private void LogGameInputDiagnosticsSnapshot(string reason)
lastReadDeviceStatus = _lastReadDeviceStatus;
}

LoggerService.LogInfo(
$"GameInputDiag reason={reason} missingReadings={missingReadingCount} repeatedTimestamps={repeatedTimestampCount} backwardTimestamps={backwardTimestampCount} deviceUnavailableRefreshes={deviceUnavailableRefreshCount} lastTimestamp={lastReadingTimestamp} lastReadHr=0x{unchecked((uint)lastReadHResult):X8} lastDeviceStatus=0x{lastReadDeviceStatus:X8}");
string message =
$"GameInputDiag reason={reason} missingReadings={missingReadingCount} repeatedTimestamps={repeatedTimestampCount} backwardTimestamps={backwardTimestampCount} deviceUnavailableRefreshes={deviceUnavailableRefreshCount} lastTimestamp={lastReadingTimestamp} lastReadHr=0x{unchecked((uint)lastReadHResult):X8} lastDeviceStatus=0x{lastReadDeviceStatus:X8}";

if (ShouldWarnGameInputDiagnostics(reason))
{
LoggerService.LogWarning(message);
}
else
{
LoggerService.LogInfo(message);
}
}
catch (Exception ex)
{
Debug.WriteLine($"[GameInput] 讀取診斷快照失敗({reason},已忽略):{ex.Message}");
}
}

/// <summary>
/// 判斷 GameInput 診斷原因是否應進入 Release 預設日誌。
/// </summary>
/// <param name="reason">診斷觸發原因。</param>
/// <returns>若應記為 Warning 則為 true。</returns>
private static bool ShouldWarnGameInputDiagnostics(string reason)
=> string.Equals(reason, "missing-reading-threshold", StringComparison.Ordinal) ||
string.Equals(reason, "device-status-non-connected", StringComparison.Ordinal);

/// <summary>
/// 安全呼叫 InputWeave runtime probe。
/// </summary>
Expand Down Expand Up @@ -2908,23 +2935,14 @@ public Task VibrateAsync(
}
catch (Exception innerEx)
{
#if DEBUG
LoggerService.LogInfo($"VibrationDiag source=GameInput stage=api action=stop outcome=failed reason=cancel-stop exception={innerEx.GetType().Name} message={innerEx.Message}");
#endif
LoggerService.LogWarning($"VibrationDiag source=GameInput stage=api action=stop outcome=failed reason=cancel-stop exception={innerEx.GetType().Name} message={innerEx.Message}");
Debug.WriteLine($"[GameInput] 取消後強制停止馬達失敗(已忽略):{innerEx.Message}");
}
}
#if DEBUG
catch (Exception ex)
{
LoggerService.LogInfo($"VibrationDiag source=GameInput stage=api action=start outcome=failed exception={ex.GetType().Name} message={ex.Message}");
}
#else
catch (Exception)
{

LoggerService.LogWarning($"VibrationDiag source=GameInput stage=api action=start outcome=failed exception={ex.GetType().Name} message={ex.Message}");
}
#endif
},
ct);
}
Expand Down Expand Up @@ -3122,9 +3140,7 @@ public void StopVibration()
}
catch (Exception bgEx)
{
#if DEBUG
LoggerService.LogInfo($"VibrationDiag source=GameInput stage=api action=stop outcome=failed reason=sync-stop-bg exception={bgEx.GetType().Name} message={bgEx.Message}");
#endif
LoggerService.LogWarning($"VibrationDiag source=GameInput stage=api action=stop outcome=failed reason=sync-stop-bg exception={bgEx.GetType().Name} message={bgEx.Message}");
Debug.WriteLine($"[GameInput] 背景停止馬達失敗(已忽略):{bgEx.Message}");
}
});
Expand All @@ -3137,9 +3153,7 @@ public void StopVibration()
}
catch (Exception ex)
{
#if DEBUG
LoggerService.LogInfo($"VibrationDiag source=GameInput stage=api action=stop outcome=failed reason=sync-stop exception={ex.GetType().Name} message={ex.Message}");
#endif
LoggerService.LogWarning($"VibrationDiag source=GameInput stage=api action=stop outcome=failed reason=sync-stop exception={ex.GetType().Name} message={ex.Message}");
Debug.WriteLine($"GameInput 停止震動失敗:{ex.Message}");
}
}
Expand Down
27 changes: 7 additions & 20 deletions src/InputBox/Core/Input/XInputGamepadController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1831,19 +1831,15 @@ public void StopVibration()

uint stopResult = XInput.XInputSetState(_userIndex, in stopVibration);

#if DEBUG
if (stopResult != 0)
{
LoggerService.LogInfo(
LoggerService.LogWarning(
$"VibrationDiag source=XInput stage=api action=stop outcome=failed reason=sync-stop result={stopResult} userIndex={_userIndex}");
}
#endif
}
catch (Exception ex)
{
#if DEBUG
LoggerService.LogInfo($"VibrationDiag source=XInput stage=api action=stop outcome=failed reason=sync-stop exception={ex.GetType().Name} message={ex.Message}");
#endif
LoggerService.LogWarning($"VibrationDiag source=XInput stage=api action=stop outcome=failed reason=sync-stop exception={ex.GetType().Name} message={ex.Message}");
Debug.WriteLine($"[XInput] StopVibration 失敗(已忽略):{ex.Message}");
}
}
Expand Down Expand Up @@ -1961,15 +1957,15 @@ public Task VibrateAsync(

uint startResult = XInput.XInputSetState(userIndex, in vibration);

#if DEBUG
if (startResult != 0)
{
LoggerService.LogInfo(
LoggerService.LogWarning(
$"VibrationDiag source=XInput stage=api action=start outcome=failed result={startResult} userIndex={userIndex} strength={safeStrength} durationMs={safeDurationMs} priority={priority}");

return;
}

#if DEBUG
bool shouldLogApiSuccess = priority != VibrationPriority.Ambient ||
Interlocked.Increment(ref _vibrationDiagSampleCounter) % 20 == 0;

Expand All @@ -1978,11 +1974,6 @@ public Task VibrateAsync(
LoggerService.LogInfo(
$"VibrationDiag source=XInput stage=api action=start outcome=ok result={startResult} userIndex={userIndex} strength={safeStrength} durationMs={safeDurationMs} priority={priority}");
}
#else
if (startResult != 0)
{
return;
}
#endif

using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(token, ct);
Expand All @@ -1998,13 +1989,11 @@ public Task VibrateAsync(
XInput.XInputVibration stop = default;
uint stopResult = XInput.XInputSetState(userIndex, in stop);

#if DEBUG
if (stopResult != 0)
{
LoggerService.LogInfo(
LoggerService.LogWarning(
$"VibrationDiag source=XInput stage=api action=stop outcome=failed reason=external-cancel result={stopResult} userIndex={userIndex}");
}
#endif
}

return;
Expand All @@ -2019,13 +2008,11 @@ public Task VibrateAsync(
XInput.XInputVibration stopVibration = default;
uint stopFinalResult = XInput.XInputSetState(userIndex, in stopVibration);

#if DEBUG
if (stopFinalResult != 0)
{
LoggerService.LogInfo(
LoggerService.LogWarning(
$"VibrationDiag source=XInput stage=api action=stop outcome=failed reason=normal-finish result={stopFinalResult} userIndex={userIndex}");
}
#endif
}, ct);
}

Expand Down Expand Up @@ -2292,4 +2279,4 @@ private void ClearAllEvents()
RightTriggerRepeat = null;
ConnectionChanged = null;
}
}
}
3 changes: 2 additions & 1 deletion src/InputBox/Core/Services/FeedbackService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ public static async Task VibrateAsync(
}
catch (Exception ex)
{
LoggerService.LogWarning($"VibrationDiag source=FeedbackService stage=dispatch outcome=failed controller={controller.GetType().Name} priority={priority} exception={ex.GetType().Name} message={ex.Message}");
Debug.WriteLine($"[震動] 控制器震動失敗(已忽略):{ex.Message}");
}
}
Expand Down Expand Up @@ -352,4 +353,4 @@ public static void EmergencyStopAllActiveControllers()
Debug.WriteLine($"緊急清理發生錯誤,已忽略:{ex.Message}");
}
}
}
}
Loading
Loading