ゲームパッドで起動するランチャー作った

# クソアプリ アドベントカレンダー

この記事は [2024 年クソアプリ アドベント カレンダー](https://qiita.com/advent-calendar/2024/kuso-app)の２日目の記事です。
**クソアプリアドベントカレンダー１０周年おめでとうございます！**

# デモ

まずはこれを

![photo-imageonline.co-7463583.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/34d405a1-7551-5c73-7b4b-cb70e7292998.gif)

んん！？　手元も映すと…

![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/1e9eeaca-4259-cd61-e330-45b86641fcae.gif)

わかりづらいので、ここだけピックアップしてみると…

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/3065ba28-312b-38d3-a5f7-5e31516c8591.gif">

こ、これは波動拳コマンド…！
波動拳コマンドを入れて Photoshop が起動しているッッ！！

(※手はゲーム廃人 妻氏の手)

# GamePad Launcher

ということで、ゲームパッドでアプリを起動するランチャーを作りました。
予めボタンの組み合わせを設定しアプリを指定しておくと、その操作をした時アプリが起動します。

ボタンの組み合わせの設定画面はこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/cbad5594-a34e-2942-d0ce-ef44f8e70240.png)

PowerPoint には[覇王翔吼拳を使わざるを得ない](https://dic.pixiv.net/a/%E8%A6%87%E7%8E%8B%E7%BF%94%E5%90%BC%E6%8B%B3%E3%82%92%E4%BD%BF%E3%82%8F%E3%81%96%E3%82%8B%E3%82%92%E5%BE%97%E3%81%AA%E3%81%84)。
昇龍拳は僕が出せないので設定しませんでした！

一番上の notepad の水色枠の部分は L, R ボタンの同時押しを表しています。
たとえば、ABXY を同時に押すコマンドだとこういう表示になります。

![Config5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/b086d18e-4243-0619-b0e8-bd4335e8b7f7.png)

実際に押した時は画面中央に↓このように表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/2e7eb3a8-a3c2-59f6-5b86-9331618cc051.png)

入力されたボタンはリアルタイムで画面中央に表示されます。

ちなみに、[この技法を使っている](https://qiita.com/pik/items/71142d607937bc713af2)のでフォーカスを奪ったりしません。
ウィンドウも半透明です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/4b6ac0ea-26bc-850d-9849-861e24e44da6.png)

また、単純に Shell Open を叩いているだけなので URL を指定すれば URL も開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/b084821e-3a0f-2006-32f5-f33761ef7a24.png)
※登録ダイアログ左側の画像部分をクリックすると任意の画像を設定できます

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/e44408d7-c64d-50b4-3451-bc49918b223a.gif" width=75%> 

実際の登録はこんな感じでゲームパッドのボタンを押しながら [+] ボタンを押すことでコマンドを登録します（アップロードに失敗するため登録シーンは一部カット＆解像度を下げてます）。

![kona-imageonline.co-6689472.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/4eff0087-c605-9b52-9e2f-59999fabc208.gif)

利用するゲームパッドを上のドロップダウンで選べます。
普段ゲームに使うメインのゲームパッドしか使えないとゲームしてるときにアプリが起動しちゃいますからね。

…このこだわりで大変なことに

# 技術紹介
ということで、大変なことになったゲームパッドのリストアップ部分です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/8423a53a-aec6-4fe3-34ae-fdfff620ef8c.png)

大変だったポイントは２つ
・ゲームパッドのリストアップ（ゲームパッドの特定）
・ゲームパッド名の取得
これについて紹介します。

## ゲームパッドのリストアップ

まずはゲームパッドのリストアップおよび特定です。

現在 Windows で使える GamePad API を調べると以下の５つがあります。

|API名|概要|リストアップ機能|固有ID|特記事項|
|---|---|---|---|---|
|[RawInput](https://learn.microsoft.com/ja-jp/windows/win32/inputdev/about-raw-input)|HIDの生データを処理する仕組み|○|△|HIDデバイス以外のゲームパッドを取得できない生データを解析する必要がある（ゲームパッド毎に違う）|
|[JoyStick](https://learn.microsoft.com/ja-jp/windows/win32/multimedia/joysticks)|WindowsのマルチメディアAPIの一部|×|×|非推奨|
|[DirectInput](https://learn.microsoft.com/ja-jp/previous-versions/windows/desktop/ee416842(v=vs.85))|DirectXの入力系API|○|○|非推奨|
|[XInput](https://learn.microsoft.com/ja-jp/windows/win32/xinput/xinput-game-controller-apis-portal)|GamePadに特化したAPI|×|×|XInput対応ゲームパッドのみ利用可<br>認識される最大の接続数は４|
|[GameInput](https://learn.microsoft.com/ja-jp/gaming/gdk/_content/gc/reference/input/gameinput/gameinput_members)|上記全てを内包するスーパーセットを目指したAPI|○|○|未実装項目が多数ある<br>Bluetoothゲームパッドが使えない|

このうち「XInput」と「GameInput」は Xbox でも使える事を企図した API です。
他の API は非推奨、Raw Input は生データを処理しないといけないので問題外。
なので実質「XInput」と「GameInput」の２つしか使えません。

最新の GameInput API は Microsoft が推奨しているのですが、未実装項目がたくさんあります。
サンプルコードで普通に未実装のメソッドを使っており、そのままコピーしても動きませんし、その機能が必要でも当然使えません。
また、バグが多く、[Bluetooth ゲームパッドが使えない](https://github.com/microsoft/GDK/issues/70)、という最悪のバグもあります（ボタン押下状態が取れない）。
さらに、ゲームパッドを認識するタイミングが「アプリが起動した時」となっていて、アプリ起動中にゲームパッドを追加しても認識されません（何度リストアップしても最初のリストしか取れません）。

これらによって GameInput ではなく XInput を使わざるを得なくなるのですが、XInput はゲームパッド固有の ID を取得できないためゲームパッドを特定できません。
（このアプリ用にどのゲームパッドを使うか覚えておきたかったため、ゲームパッド固有 ID が必須でした）

そこで苦肉の策として DirectInput でゲームパッドのリストアップと固有 ID を取得し、実際の入力では XInput を使う、という方法にしました。
DirectInput が非推奨 API なので、当初は GameInput を使ったのですが上記したようにアプリ起動中の挿抜検知ができないため断念しました。

ということで DirectInput を使う事にしたのですが、ここで問題になるのが DirectInput で返ってくる ID が XInput のゲームパッドのどれにあたるかわからない、ということです。
XInput はゲームパッドを 0～3 の番号でしか管理してないので…

色々調べたところ XInput の[隠し API XInputGetCapabilitiesEx](https://gist.github.com/DJm00n/0f0563702f6cf01e8812ebc760a78cf1) を使うと「ベンダーID」と「プロダクト ID」を取れることがわかりました。
[DirectInput でも GUID から「ベンダーID」と「プロダクトID」が取れる](https://learn.microsoft.com/ja-jp/windows/win32/xinput/xinput-and-directinput#xinput-and-directinput-side-by-side)ので、比較すればゲームパッドを特定できそうです。

できそうでした…
違う製品のくせに同じ「プロダクトID」を返してくるメーカーがなければ…
仕方が無いので同じプロダクトIDを持ったゲームパッドが２つ以上見つかった場合、名前を or で繋げることにしました（最初の図に or で繋がった名前があります）。

## ゲームパッド名の取得

次にデバイス名の取得です。
DirectInput で取れるゲームパッドの情報が「Bluetooth XINPUT 互換入力デバイス」とかになっている場合があり、製品名を取れないことがあります。
ですがデバイスマネージャーでは取れてるので何か方法があるだろうと試行錯誤しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/cda8e34f-55f3-ca72-609b-3cf34b547843.png)

試行錯誤した結果 [WMI](https://learn.microsoft.com/ja-jp/windows/win32/wmisdk/wmi-start-page) の [Win32_PnPEntity](https://learn.microsoft.com/ja-jp/windows/win32/cimwin32prov/win32-pnpentity) を使えば取れそうでした。

で、結局取れはしたのですが、すごく大変でした。
基本は前述の「ベンダーID」「プロダクトID」を比較して Win32_PnPEntity の Caption プロパティを取ってくるのですが、これの中身が入っていなかったり、入っていても「HID 準拠ゲーム ゲームパッド」だったりするので、この２つの ID から本物の名前を取る小細工をたくさんする必要がありました…（しかも、まだ上手くいっていない
詳しくはソースの [PK.Device.GamePad.Win.pas](https://github.com/freeonterminate/XPadLauncher/blob/main/Lib/Device/PK.Device.GamePad.Win.pas) を見てください。

## XInput の使い方
今回メインで利用している XInput についてです。
XInput は非常に単純な API になっていてボタンの押下状態の取得はこれだけです。
DirectInput のような初期化処理も必要ありません。いきなり呼び出せます。

```delphi:例：0番目のGamePadの押下状態を取得
var State: TXInputState;
if XInputGetState(0, State) <> ERROR_SUCCESS then
  Exit;

// Aボタンが押されているか
if (State.Gamepad.wButtons and XINPUT_GAMEPAD_A) <> 0 then
  Log.d('A ボタンが押されている');

// Bボタンが押されているか
if (State.Gamepad.wButtons and XINPUT_GAMEPAD_B) <> 0 then
  Log.d('B ボタンが押されている');
```

[XInputGetState](https://learn.microsoft.com/ja-jp/windows/win32/api/xinput/nf-xinput-xinputgetstate) を定期的に呼んであげればボタンの押下状態がとれるということです。
Game を作る上で必要な [ゲームループ](https://qiita.com/pik/items/fa80fa4b942bb6c2a79a)向きの使いやすい実装ですね。

## コマンドの処理
アルゴリズム的な説明もすこし。
コマンドの実装についてです。

今回は、こんな仕組みにしました。

```A, B, C``` でコマンドが発動する事とします。

まず、バッファとバッファの入力位置を示すインデックスポインタを用意します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/8c839837-6577-79b1-79fc-bb8b05d52280.png)

入力があったら、インデックスポインタが指す位置に入力を入れて、ポインタを１つ動かします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/904622ac-5e29-f87f-0d73-8aa4679e126e.png)

続く入力が一定時間内になかった場合、ポインタを 0 の位置に戻します。
0 に戻ると有効な入力も 0 になってしまうので、コマンドが成立しません。

バッファ内の有効ボタン数とインデックスが指す値は常に同じになるため、インデックスポインタの値を見るだけで有効な入力がわかる仕組みです（for 文などで処理しやすくなります）。

一定時間でインデックスポインタが０に戻るため、格ゲーのコマンド入力が難しくなっているわけですね。

また、コマンドは最長一致にしています。
そうでないと「覇王翔吼拳」を出しているときに「波動拳」が出てしまうためです。

## 利用した外部サービス

次に今回利用したサービスです。

### Kenny Input Prompts
https://kenney.nl/assets/input-prompts
CC0 ライセンスで公開されているゲームパッドのドット絵群。
可愛い。CC0 で改変もできるため今回は改変して使用
でも無料だと気がひけるクオリティなので、[全てのアセットが使える有料版](https://kenney.itch.io/kenney-game-assets)を購入しました。$19.95 以上で買えます。

### Simple WMI View
https://www.nirsoft.net/utils/simple_wmi_view.html
WMI の値を手軽にチェックできるツール。
当然 Query も発行できるので普通に便利です。
これを使って Win32_PnPEntity の結果とにらめっこしながら名前を取得する部分を書きました。

### Chet
https://github.com/neslib/Chet
Clang を使ったソース解析で C/C++ のソースを Object Pascal のソースに変換してくれるツール。
使い方には大分くせがあったけど、これを使って GameInput API を移植しました。GameInput は [COM（ライト）](https://learn.microsoft.com/ja-jp/gaming/gdk/_content/gc/input/overviews/input-fundamentals)ベースの API ですが、ちゃんと変換できました。
…結局 GameInput API 使わなかったんですけどね

### DelphiStyles.com
https://delphistyles.com/fmx/index.html
美麗な Delphi 用 Style といえばここ。
GetIt でいくつかの Style が提供されているけど背景が単色ではないものは、こっちの有料版の方が多いのでお金に余裕があるならおすすめ。

ただ、[KSDev の FireMonkey の生みの親 Eugene Kryukov 氏](https://blogs.embarcadero.com/ja/eugene-kryukov-father-of-firemonkey-and-incredibly-talented-developer-ja/)が亡くなったので今後どうなるか…（[DelphiStyles.com は KSDev グループ](https://www.turbococoa.com/product.html)）
FMX を便利に使わせていただいている身としては、Eugene Kryukov 氏の訃報は衝撃が大きい…ご冥福をお祈りします。

### GIFスピードチェンジャー
https://gifspeed.imageonline.co/index-jp.php#google_vignette
このアプリの製作とは関係ないですが記事を書く上で有用だったサービス。
いつも Qiita に上げると GIF の再生スピードが落ちてガッカリしてたんだけど、ここでスピードを最速にすると求めている速さになりました。
処理速度もめちゃくちゃ速いのでオススメ。

## 検証した GamePad

最後に今回検証したゲームパッドです。
(半分が妻氏のもの…とんでもないゲーマーなので)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/818456ac-6f9b-d67d-7c31-6633e5907110.png)

大体のコントローラーはちゃんとしていたのですが、その中でもビックリしたのが右上の中華ゲームパッド…こいつ XInput のインジケーター（リング）がちゃんと付いてるし、Xbox 360 Controller っていう文字列返してくれるし一番仕様に準拠してた…恐るべし中華ブランドと思ったけど[純正品の流用](https://worktoolsmith.com/xbox360-compatible-controller-review-1/)なのかもしれない。

純正 Xbox ゲームパッドも、もちろんちゃんとしてました。
２つのゲームパッドは当然ながらプロダクトIDは別でした（ちなみに Xbox Design Lab は[こちら](https://xboxdesignlab.xbox.com/ja-jp/)）。

[BIGBIG WON Gale](https://jp.bigbigwon.com/product/controller/gale/) も正しく情報を返してくれました。
この中だと性能は抜群です。
FPS やる人には良さそう。

反面残念だったのが評価の高い 8bitDo のゲームパッド。多分使う人は１つの製品しか使わないだろうという事なのか Pro2 と Zero2 で同じプロダクトIDになっていてどっちか判別できませんでした。
とはいえ、純正品以外のゲームパッドで同じメーカーの物を２つ以上持っていないので実際は他のメーカーもそうなのかも知れません。

DualShock4 は XInput で利用できないけど GameInput だとどうかな？という確認用でしたが、GameInput が使えなかったので結局利用せず…

個人的に手に馴染んでるのは [8bitDo Pro 2](https://www.8bitdo.com/pro2/)…でもプロダクトIDの重複でうーんという感じ。

また、Bluetooth GamePad は一定時間操作がないと切れてしまう物がほとんどなので、このアプリで使うためには有線ゲームパッドじゃないとダメそう…ということは、あんなに苦労せずとも良かったのでは？

# まとめ
ゲームパッドが余っていたら使えそうで微妙に使いづらい、絶妙なクソアプリになったのではないでしょうか！

## API について
GameInput API は人類にはまだ早い。

現状の方針は
・基本 XInput を使う
・個々のコントーラーの識別が必要なら DirectIpunt も併用する
となります。

## バイナリ
こちらからダウンロードできます。
https://piksware.com/xpadlauncher

せっかくだから自分のサイトにアップしときました！

## ソース
ソースはこちらに。
https://github.com/freeonterminate/XPadLauncher

今回は僕が普段使っている拙作のライブラリ群もコピーして入れてあります。
VCL 用のライブラリはたくさんありますが、FMX 用のはそんなにないため参考になれば。

### ビルド環境

[Delphi 12.2 Athens](https://forest.watch.impress.co.jp/docs/news/1624386.html) [Patch2](https://blogs.embarcadero.com/ja/rad-studio-12-2-athens-patch-2-available-ja/) で製作。恐らく無料の [Community Edition](https://www.embarcadero.com/jp/products/delphi/starter) でもビルドできるはず。

今回は Windows オンリーです（Windows 10 以上）。
恐らく以下のファイルを追加したり削除すれば Mac にも対応するような気がします。
ただ、わかりやすくするため全てのライブラリをプロジェクトに登録しているので、Lib 以下のファイルはプロジェクトから外す必要があります。

#### Mac 用の機能を実装する必要がある Unit
PK.AutoRun.Mac.pas
PK.Device.GamePad.Mac.pas
PK.Graphic.IconConverter.Mac.pas
PK.Graphic.IconUtils.Mac.pas

#### IFDEF で排除する Windows 用 Unit
PK.HardInfo.WMI.Win.pas
PK.GUI.DarkMode.Win.pas
PK.GUI.NativePopupMenu.Win.pas
Winapi.XInput.pas

# おまけ
## SendKey との組み合わせ

僕のウェブサイトから[ダウンロードできる SendKey](https://www.piksware.com/sendkey) と組み合わせるとボタンを押したときにショートカットキーを送信できます。
例えば ```Alt+Shift+Ctrl+S``` を登録すれば、Photoshop で「Web 用に保存」が実行できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/16a895ad-ea51-a5d3-daaf-87de37d1cf5e.png)

![shortcut-imageonline.co-3581529.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/b92c378c-d758-c9cd-49d6-04b25995dda7.gif)

## Delphi 紹介
Delphi 大好きなのでこの項は譲れない。
Windows, macOS, iOS, Android, Linux のアプリケーションを作れるのは良く言っていますが、Delphi は、こんな風に部品をドラッグ＆ドロップするだけで作れるので楽ちんで本当にオススメ。UI 構築で[コードを書く必要はありません！](https://qiita.com/pik/items/a0dc426b2089dbce8bc3)

![DesignForms.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/2d4a832a-7f0c-4c9f-59f6-8168457dcb3c.png)
（×表記になっている部分は BitmapList を使い回しているため）

それとコンパイル型言語とは思えないレベルのビルドの速さも良いですよ！
このプロジェクト一式（33ファイル）フルビルドしても 600[msec]！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/4450db48-f7b8-e9ad-f80e-9bde7e5af969.png)

## その後のゲームパッド達

タワーマンションに居住しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/dad07be0-fa82-a83a-6ff4-9029558829ad.png)

[MANBA ONE](https://www.amazon.co.jp/dp/B0D2JYV5D9) は液晶付いてて便利そうだし  [GameSir Tarantula Pro](https://www.amazon.co.jp/dp/B0DFM8L3YH) は[ボタンが変わるギミック](https://www.amazon.co.jp/vdp/28a15dd8e0d94037ad64763e0e965c31)がスゴイので、ほしい…コントローラー沼