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
12 changes: 9 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# XInputXFire Proxy DLL
cmake_minimum_required(VERSION 3.20)
project(XInputXFireProxy LANGUAGES CXX)
project(XInputXFireProxy LANGUAGES CXX VERSION 1.0.1)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand All @@ -23,7 +23,10 @@ add_library(XInputProxy SHARED
)
target_include_directories(XInputProxy PRIVATE src/proxy)
target_compile_definitions(XInputProxy PRIVATE
WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0602 NOMINMAX)
WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0602 NOMINMAX
XFIRE_VERSION_MAJOR=${PROJECT_VERSION_MAJOR}
XFIRE_VERSION_MINOR=${PROJECT_VERSION_MINOR}
XFIRE_VERSION_PATCH=${PROJECT_VERSION_PATCH})
if(MSVC)
target_compile_options(XInputProxy PRIVATE /W4 /permissive- /utf-8)
endif()
Expand Down Expand Up @@ -76,7 +79,10 @@ add_executable(xfire_unit
)
target_include_directories(xfire_unit PRIVATE src/proxy)
target_compile_definitions(xfire_unit PRIVATE
XFIRE_TEST WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0602 NOMINMAX)
XFIRE_TEST WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0602 NOMINMAX
XFIRE_VERSION_MAJOR=${PROJECT_VERSION_MAJOR}
XFIRE_VERSION_MINOR=${PROJECT_VERSION_MINOR}
XFIRE_VERSION_PATCH=${PROJECT_VERSION_PATCH})
if(MSVC)
target_compile_options(xfire_unit PRIVATE /W4 /permissive- /utf-8)
endif()
Expand Down
38 changes: 28 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# XInputXFire Proxy DLL

Xboxコントローラ(XInput)の **R2 または L2 トリガーを押している間だけ、方向キー(DPAD)と ABXY ボタンを連射** するツール。XInput Plus のプロキシDLL方式を参考に自作。
Xboxコントローラ(XInput)の **RT または LT トリガーを押している間だけ、方向キー(DPAD)と ABXY ボタンを連射** するツール。XInput Plus のプロキシDLL方式を参考に自作。

## 動作方式(プロキシDLL方式)

Expand All @@ -12,27 +12,37 @@ game.exe ─(暗黙DLL解決)─▶ ローカル xinput1_3.dll [プロキシ]
本物 System32\xinput1_3.dll (or 1_4) に転送 → 生state取得
連射エンジン: L2/R2判定 → QPC周期 → DPAD/ABXY 改変
連射エンジン: LT/RT判定 → QPC周期 → DPAD/ABXY 改変
▼ (トリガ値は触らない)
ゲームへ改変後 state を返す
```

## 機能(連射ロジック)

- **発動条件**: L2 または R2 が押下閾値(既定150/255)以上。いずれかで発動。
- **マスター切替(LB+A)**: 連射機能の有効/無効を **LB+A 同時押し(PS名 L1+×)** の立ち上がりでトグル。**起動時は OFF**(メニュー操作中の誤動作を防ぐため)。OFF 中は LT/RT を押しても連射せず入力をそのまま通す。
- **発動条件**: LT または RT が押下閾値(既定150/255)以上。いずれかで発動。マスター ON のときのみ有効。
- **連射対象**: DPAD(上下左右) + ABXY(ini で個別指定可)。
- **周期**: QPC 高精度タイマベース。ON区間(既定50ms)=ボタン押下、OFF区間(既定50ms)=ボタン離。ポーリング間隔(60/120Hz等)に依存しない。
- **初回ON区間(FirstOnMs)**: 対象ボタン押下直後の最初のON区間だけ専用の長さを指定(既定200ms・0=無効=OnMsと同値)。位相クロックは対象ボタン押下基準で、ボタンを離すとリセットされ次回押下で再びFirstOnMsから。`FirstOnMs` を大きくすれば連射ON中でも「1回だけボタンを押す」がコントロール可能(長い初回ONの間に離せば1回押下で確定)。
- **ヒステリシス**: 押下閾値150 / 離上閾値140 で、トリガが閾値付近でチャタリングしても連射が暴走しない。
- **4コントローラ独立**: dwUserIndex 0-3 各々独立状態。
- **トリガ透過**: L2/R2 のアナログ値はゲームへそのまま伝達(連射は DPAD/ABXY のみに作用)。
- **トリガ透過**: LT/RT のアナログ値はゲームへそのまま伝達(連射は DPAD/ABXY のみに作用)。
- **物理押下のみ連射**: 物理的に押されている対象ボタンだけ連射。離せば停止。

> **ボタン名対応**: 本ツールは XInput(Xbox API) ベースのため Xbox 名を基本表記します。PS コントローラでの呼び名との対応は以下の通り。
>
> | Xbox | PS | XInput API |
> |---|---|---|
> | LT / RT | L2 / R2 | `bLeftTrigger` / `bRightTrigger`(連射発動トリガ・アナログ) |
> | LB / RB | L1 / R1 | `XINPUT_GAMEPAD_LEFT_SHOULDER` / `XINPUT_GAMEPAD_RIGHT_SHOULDER`(デジタル肩) |
> | A / B / X / Y | × / ○ / □ / △ | `XINPUT_GAMEPAD_A/B/X/Y`(顔ボタン・位置対応) |

## 設定ファイル (XInputXFire.ini)

ゲームexeと同フォルダに配置。無い場合はデフォルト値を使用。

```ini
; XInputXFire v1.0.1
[XFire]
OnMs=50
OffMs=50
Expand All @@ -41,11 +51,19 @@ FirstOnMs=200
TriggerThreshold=150
HysteresisLow=140
TargetButtons=DPAD_UP|DPAD_DOWN|DPAD_LEFT|DPAD_RIGHT|A|B|X|Y
EnableL2=1
EnableR2=1
EnableLT=1
EnableRT=1
; 連射マスター切替コンボ(|区切り・既定 LB|A=PS名 L1+A)。全キー同時押しの立ち上がりで ON/OFF 切替。空で無効(常時ON運用)。
ToggleButtons=LB|A
; 起動時のマスター状態(1=ON / 0=OFF・既定0=OFF)。OFF中は連射せず素通し(メニュー操作安全)。
DefaultEnabled=0
; トグル切替時の音声アナウンス(1=有効 / 0=無効・既定1)。SAPI で "Enabled/Disabled cross fire." を再生。
AnnounceEnabled=1
; 起動音(1=再生 / 0=無効・既定1)。DLL 埋め込み WAVE リソースを再生しプロキシロード完了を通知。
StartupSound=1
```

`TargetButtons` 対応名: `DPAD_UP` `DPAD_DOWN` `DPAD_LEFT` `DPAD_RIGHT` `A` `B` `X` `Y` `LB` `RB` `START` `BACK` `LSB` `RSB`(`|` 区切り)
`TargetButtons` / `ToggleButtons` 対応名: `DPAD_UP` `DPAD_DOWN` `DPAD_LEFT` `DPAD_RIGHT` `A` `B` `X` `Y` `LB` `RB` `START` `BACK` `LSB` `RSB`(`|` 区切り)

## プロキシDLLビルド

Expand Down Expand Up @@ -81,7 +99,7 @@ ctest --test-dir build-x64 --output-on-failure
## 検証

1. **単体テスト** (`xfire_unit`): モックQPC時刻注入で連射ロジック(ON/OFF周期・ヒステリシス・トリガ透過・対象外保護・4コントローラ独立)を検証。コントローラ不要。
2. **テストハーネス** (`test_harness`): プロキシDLLを同フォルダに `xinput1_3.dll` として配置し実行。実コントローラの R2 押下中に対象ボタンが周期トグルするか CSV ログで確認(60秒)。
2. **テストハーネス** (`test_harness`): プロキシDLLを同フォルダに `xinput1_3.dll` として配置し実行。実コントローラの RT 押下中に対象ボタンが周期トグルするか CSV ログで確認(60秒)。
- **Smart App Control(SAC) 有効環境では test_harness.exe が起動できない場合があります**。SAC は未署名かつ Microsoft クラウドに実績のない新規バイナリを「未確認」としてブロックし(パスベースではないため Program Files 等へのコピーでも回避不可)、ユーザ側の例外設定もありません。イベントログ `Microsoft-Windows-CodeIntegrity/Operational` の ID 3118/3033 で「Smart App Control Block」を確認できます。
- なお**本番のプロキシDLLは SAC 下でもブロックされません**(署名済みゲーム exe が LoadLibrary で読み込む DLL は許容されるため)。SAC に引っかかるのは test_harness 等の**独立未署名 exe のみ**です。test_harness が起動できない場合は、SAC オフの別PC/VM で検証するか、本番ゲーム経路で実機確認してください。
3. **統合**: 許可されたオフラインゲームに配置し実動作確認。
Expand All @@ -101,9 +119,9 @@ ctest --test-dir build-x64 --output-on-failure

コントローラが無反応・連射が効かない場合、プロキシDLLは起動時(初回エクスポート呼出時)の診断結果を **`%TEMP%\XInputXFire_xinput.log`** に1行ずつ追記します。このログで原因を切り分けられます。

- `[STICKYINIT] LoadOnce=1 hDll=... GetState=...` → プロキシが正常にロードされ、本物DLLの関数ポインタを取得した(`hDll=0000000000000000` なら本物DLLロード失敗)。
- `[STICKYINIT] version=<ビルドバージョン> LoadOnce=1 hDll=... GetState=...` → プロキシが正常にロードされ、本物DLLの関数ポインタを取得した(`version=` はビルドバージョン、`hDll=0000000000000000` なら本物DLLロード失敗)。
- `[LOADER] ...` → 本物DLLのロード失敗・必須エクスポート欠落等(フォールバック先DLLの切り替え状況)。
- `[CONFIG] ...` → ini の値が非数値・範囲外・不明トークンで既定値に置換された(意図しない挙動の原因特定に)。
- `[CONFIG] ...` → ini の値が非数値・範囲外・不明トークンで既定値に置換された、または廃止キー(`EnableL2`/`EnableR2`)が検出された(意図しない挙動の原因特定に)。
- `[XFIRE] QueryPerformanceFrequency returned 0 ...` → 高精度タイマ取得失敗(連射機能が無効化・パススルーのみ動作)。

**プレイ中はこのログは増えません**。診断行は起動時のみ書かれ、毎フレームの `XInputGetState` / 連射処理のホットパスはログを書きません(長時間プレイでも肥大化しません)。ログが `[STICKYINIT]` 1行だけで後が続かない場合はプロキシは正常に動いているので、コントローラ無反応は「ゲームが XInput 経由で入力を取得していない(非対応)」の可能性が高いです(→ [対応ゲームの条件](#対応ゲームの条件))。
Expand Down
10 changes: 5 additions & 5 deletions assets/XInputXFire.ini
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
; XInputXFire Proxy DLL 設定ファイル
; XInputXFire Proxy DLL 設定ファイル (v1.0.1)
; 対象ゲームのexeと同じフォルダに配置してください。
; ファイルが無い場合はデフォルト値(下記)が使用されます。

Expand All @@ -12,7 +12,7 @@ OffMs=50
; ボタンを離すとリセットされ、次回押下で再びこの長さから開始。
FirstOnMs=200

; トリガ押下判定の閾値(0-255)。L2/R2 がこの値以上で連射発動。
; トリガ押下判定の閾値(0-255)。LT/RT がこの値以上で連射発動。
TriggerThreshold=150
; 離上判定の閾値(0-255)。連射中はこの値未満で解除(ヒステリシス=チャタリング防止)。
HysteresisLow=140
Expand All @@ -21,9 +21,9 @@ HysteresisLow=140
; 対応名: DPAD_UP DPAD_DOWN DPAD_LEFT DPAD_RIGHT A B X Y LB RB START BACK LSB RSB
TargetButtons=DPAD_UP|DPAD_DOWN|DPAD_LEFT|DPAD_RIGHT|A|B|X|Y

; L2/R2 を連射発動トリガとして使用(1=有効 / 0=無効)
EnableL2=1
EnableR2=1
; LT/RT を連射発動トリガとして使用(1=有効 / 0=無効)
EnableLT=1
EnableRT=1

; ---- 連射マスタートグル(常時有効だとメニュー開閉ボタンが連射して誤動作するため) ----
; 連射マスター有効/無効を切替えるコンボキー(|区切り)。この全キー同時押下の
Expand Down
22 changes: 18 additions & 4 deletions src/proxy/Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ void Config::LoadOnce() {
};
// bool 項目:1/0/true/false/yes/no/on/off を受理。空(キー不在/空)=既定、
// 不明値=既定+ログ。従来は GetPrivateProfileIntW で "true"/"yes" 等が 0(無効)に黙って変換
// されていた(EnableL2=true で L2 連射が黙って OFF になる等)。
// されていた(EnableLT=true で LT 連射が黙って OFF になる等)。
auto readBool = [sec, &ini](const wchar_t* key, const char* label, bool defv) -> bool {
wchar_t b[32] = {0};
GetPrivateProfileStringW(sec, key, L"", b, 32, ini.c_str());
Expand All @@ -90,9 +90,23 @@ void Config::LoadOnce() {
g_cfg.firstOnMs = readClampedDw(L"FirstOnMs", "FirstOnMs", g_cfg.firstOnMs, (INT)Config::kFirstOnMsMin, (INT)Config::kOnMsMax);
g_cfg.triggerThreshold = readClampedByte(L"TriggerThreshold", "TriggerThreshold", g_cfg.triggerThreshold);
g_cfg.hysteresisLow = readClampedByte(L"HysteresisLow", "HysteresisLow", g_cfg.hysteresisLow);
// EnableL2/R2 は 1/0/true/false/yes/no/on/off で記録(不明値=既定+ログ)。
g_cfg.enableL2 = readBool(L"EnableL2", "EnableL2", g_cfg.enableL2);
g_cfg.enableR2 = readBool(L"EnableR2", "EnableR2", g_cfg.enableR2);
// EnableLT/RT は 1/0/true/false/yes/no/on/off で記録(不明値=既定+ログ)。
g_cfg.enableLT = readBool(L"EnableLT", "EnableLT", g_cfg.enableLT);
g_cfg.enableRT = readBool(L"EnableRT", "EnableRT", g_cfg.enableRT);
// 廃止キー(EnableL2/EnableR2)の検出: クリーンブレイクで値は読まないが、
// 移行時にユーザの意図が黙って反転(無効化->既定true=有効化)するのを防ぐため存在を警告する。
struct LegacyKey { const wchar_t* oldName; const wchar_t* newName; };
for (const LegacyKey& lk : { LegacyKey{L"EnableL2", L"EnableLT"}, LegacyKey{L"EnableR2", L"EnableRT"} }) {
wchar_t lb[32] = {0};
GetPrivateProfileStringW(sec, lk.oldName, L"", lb, 32, ini.c_str());
if (lb[0] != L'\0') {
char msg[160];
sprintf_s(msg, sizeof(msg),
"[CONFIG] %ls is removed (renamed to %ls); ignored. Use the new key.",
lk.oldName, lk.newName);
DiagLog::Log(msg);
}
}
wchar_t buf[256] = {0};
GetPrivateProfileStringW(sec, L"TargetButtons", L"", buf, 256, ini.c_str());
int unk = 0;
Expand Down
6 changes: 3 additions & 3 deletions src/proxy/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ struct XFireConfig {
| XINPUT_GAMEPAD_DPAD_LEFT | XINPUT_GAMEPAD_DPAD_RIGHT
| XINPUT_GAMEPAD_A | XINPUT_GAMEPAD_B
| XINPUT_GAMEPAD_X | XINPUT_GAMEPAD_Y; // 連射対象ビットマスク(既定=DPAD全方向+ABXY)
bool enableL2 = true;
bool enableR2 = true;
// --- 連射マスタートグル(L2/R2 ゲートの上掛け) ---
bool enableLT = true;
bool enableRT = true;
// --- 連射マスタートグル(LT/RT ゲートの上掛け) ---
WORD toggleButtons = XINPUT_GAMEPAD_LEFT_SHOULDER | XINPUT_GAMEPAD_A; // トグル切替コンボ(LB|A)。全ビット同時押下の立ち上がりで ON/OFF 切替
bool defaultEnabled = false; // 起動時のマスター状態(false=OFF・メニュー操作安全)
bool announceEnabled = true; // トグル切替時の SAPI 音声アナウンス有無
Expand Down
21 changes: 21 additions & 0 deletions src/proxy/Version.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Version.h - ビルドバージョン(CMake から XFIRE_VERSION_MAJOR/MINOR/PATCH を数値マクロで注入)。
// 数値を文字列化して "MAJOR.MINOR.PATCH" を構成。文字列マクロのジェネレータ依存の
// クォート問題を回避するため数値で渡してここで組み立てる。未注入時は 0.0.0 フォールバック。
#pragma once
// フォールバックは MAJOR/MINOR/PATCH 個別にガードする。1つだけ定義(部分注入)でも
// 残りが既定0になるため、異常な中途半端バージョンにならず安全。
#ifndef XFIRE_VERSION_MAJOR
#define XFIRE_VERSION_MAJOR 0
#endif
#ifndef XFIRE_VERSION_MINOR
#define XFIRE_VERSION_MINOR 0
#endif
#ifndef XFIRE_VERSION_PATCH
#define XFIRE_VERSION_PATCH 0
#endif
#define XFIRE_VERSION_STR_(a) #a
#define XFIRE_VERSION_STR(a) XFIRE_VERSION_STR_(a)
#define XFIRE_VERSION \
XFIRE_VERSION_STR(XFIRE_VERSION_MAJOR) "." \
XFIRE_VERSION_STR(XFIRE_VERSION_MINOR) "." \
XFIRE_VERSION_STR(XFIRE_VERSION_PATCH)
8 changes: 4 additions & 4 deletions src/proxy/XFireEngine.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// XFireEngine.cpp - 連射タイミング制御コア
// 連射マスタートグル(既定OFF)の上に、L2/R2 押下中のみ連射対象ボタン(DPAD/ABXY)を
// 連射マスタートグル(既定OFF)の上に、LT/RT 押下中のみ連射対象ボタン(DPAD/ABXY)を
// QPC ベース周期で ON/OFF 反転。トリガ値(bLeftTrigger/bRightTrigger)は触らない。
// 4コントローラ独立。コンボキー(LB+A)立ち上がりでマスターON/OFF切替 + SAPI 音声。
#include "XFireEngine.h"
Expand Down Expand Up @@ -98,10 +98,10 @@ void XFireEngine::Apply(DWORD idx, XINPUT_STATE* p) {
return;
}

// 有効なトリガの最大値(L2/R2 いずれか押下で発動)
// 有効なトリガの最大値(LT/RT いずれか押下で発動)
BYTE trig = 0;
if (cfg.enableR2 && g.bRightTrigger > trig) trig = g.bRightTrigger;
if (cfg.enableL2 && g.bLeftTrigger > trig) trig = g.bLeftTrigger;
if (cfg.enableRT && g.bRightTrigger > trig) trig = g.bRightTrigger;
if (cfg.enableLT && g.bLeftTrigger > trig) trig = g.bLeftTrigger;

// ヒステリシス: 押下判定は高閾値、離上判定は低閾値
bool pressed = s.active ? (trig >= cfg.hysteresisLow)
Expand Down
4 changes: 2 additions & 2 deletions src/proxy/XFireEngine.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// XFireEngine.h - 連射タイミング制御コア
// 連射マスタートグル(既定OFF)の上に L2/R2 押下中のみ、連射対象ボタン(DPAD/ABXY)を
// 連射マスタートグル(既定OFF)の上に LT/RT 押下中のみ、連射対象ボタン(DPAD/ABXY)を
// QPC ベースの周期で ON/OFF 反転。トリガ値(bLeftTrigger/bRightTrigger)は触らない
// (ゲームへそのまま伝達)。4コントローラ(dwUserIndex 0-3)は独立状態。
#pragma once
Expand All @@ -9,7 +9,7 @@ namespace XFireEngine {
// 初回エクスポート呼出時に1回だけ実行(QPC周波数取得・マスター既定状態設定)。スレッドセーフ。
void InitOnce();
// XInputGetState 転送後に呼ぶ。コンボキー(LB+A)立ち上がりでマスターON/OFF切替。
// マスターONかつ L2/R2 押下中なら pState の DPAD/ABXY を連射改変。トリガ値は変更しない。
// マスターONかつ LT/RT 押下中なら pState の DPAD/ABXY を連射改変。トリガ値は変更しない。
// 位相クロックは対象ボタン押下基準: 押下直後の最初のON区間は FirstOnMs(0=OnMsと同値)、
// その後 OnMs/OffMs サイクル。対象ボタンを離すと位相リセットされ次回押下でFirstOnMsから再開。
void Apply(DWORD dwUserIndex, XINPUT_STATE* pState);
Expand Down
5 changes: 3 additions & 2 deletions src/proxy/exports.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "XFireEngine.h"
#include "Announcer.h"
#include "DiagLog.h"
#include "Version.h"
#include <atomic>
#include <cstdio>

Expand All @@ -27,8 +28,8 @@ void StickyInit() {
g_stickyDone.store(1, std::memory_order_release);
char b[256];
const RealXInput& r = RealXInputLoader::Get();
sprintf_s(b, "[STICKYINIT] LoadOnce=%d hDll=%p GetState=%p SetState=%p Cap=%p Enable=%p",
ok ? 1 : 0, r.hDll, (void*)r.GetState, (void*)r.SetState, (void*)r.GetCapabilities, (void*)r.Enable);
sprintf_s(b, "[STICKYINIT] version=%s LoadOnce=%d hDll=%p GetState=%p SetState=%p Cap=%p Enable=%p",
XFIRE_VERSION, ok ? 1 : 0, r.hDll, (void*)r.GetState, (void*)r.SetState, (void*)r.GetCapabilities, (void*)r.Enable);
DiagLog::Log(b);
Announcer::StartupBeep(); // 初回エクスポート(プロキシロード)完了をビープで通知
}
Expand Down
Loading
Loading