Zuiki製のワンハンドルコントローラを使ってエミュレータ上の電車でGO！(PS1版)を遊ぶ

某所に書いたメモ書きをちょっとだけ書き換えてコピペ。

# とにかく早く遊びたい人向け
https://github.com/sonarAIT/ZuikiController2PS1DenshaDeGo/tree/main
これをクローンして`make run`すれば動きます。エミュレーターのキーマップだけ以下の通りにしてください。あと、Windowsにおけるキーボードの設定で、Microsoft IME(日本語+英語)ではなくUSキーボード(英語のみ)を使用してください。
![](https://i.gyazo.com/4dc27cdf87972763b63fa7821521490c.png)

あと、エミュレータとかじゃなくて普通にWindows版の電車でGO!で遊びたい人は、以下のサイトを見ると幸せになれると思います。
https://autotraintas.hariko.com/

# 基本方針
- Zuiki製のワンハンドルコントローラ(新しい)で電車でGO！(PS1版)を遊ぶ。
    - https://www.zuiki.co.jp/mascon/
- 電車でGO！(PS1版)にも専用コントローラが存在する。その専用コントローラを、Zuiki製のワンハンドルコントローラで代替する形で実装を行う。
- 電車でGO！(PS1版)の専用コントローラは、あくまでコントローラに過ぎない。専用コントローラは、複雑なコマンドをPS1に送り続けるための装置であると言える。
    - ゲーム側で専用コントローラの接続を認識するためのコマンドが存在し、それが入力されているか否かで、通常のコントローラか専用のコントローラかを識別しているらしい。
    - 物凄く極端なことを言えば、実機の通常のコントローラで同じ操作をすれば、専用コントローラと誤認識させた状態でプレイすることができる。
    - この仕組みを利用して、専用コントローラが接続されているかのように誤認識させる形で実装を行う。
- Zuiki製のコントローラから入力を受け取り、それを電車でGO！が認識できる形に変換し、エミュレータに入力すれば、Zuiki製のコントローラでエミュレータ上の電車でGO！(PS1版)を遊ぶことができる。
    - エミュレータへの入力は、単にマクロによるキーボード入力で行う。

# モジュール設計
- 入力モジュール
    - Zuiki製ワンハンドルコントローラからの入力を受け取る。
- 変換モジュール
    - 変換を実行する。変換に必要な状態があれば、保持する。
- 出力モジュール
    - キーボード入力を実際に実行する。

コントローラを変えるときは、入力モジュールと変換モジュールを書き換えれば良い。
出力先のエミュレータ・ゲームを変える時は、変換モジュールと出力モジュールを書き換えれば良い。

# Zuiki製のコントローラーから入力を受け取る
https://qiita.com/10_tenk/items/973d78eff7bb3d9de3a2
この記事にまんま書いてある。

# エミュレータにマクロによるキーボード入力を行う
「複雑なコマンド」を入力して、実際に電車でGO！をPythonから操作できるかどうかを確かめる。
「複雑なコマンド」は以下のリポジトリから確認できる。
https://github.com/KeKe115/Densya_de_go

| / | left | down | right | triangle | r1 | l1 | r2 | l2 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | 
| P5 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 |
| P4 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 1 |
| P3 | 1 | 1 | 0 | 1 | 1 | 0 | 1 | 1 |
| P2 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 1 |
| P1 | 0 | 1 | 1 | 1 | 1 | 0 | 1 | 1 |
| N | 1	| 1	| 1	| 0	| 1	| 0	| 1	| 1 |
| B1 | d | d | d | d | 1 | 1 | 1 | 0 |
| B2 | d | d | d | d | 1 | 0 | 1 | 0 |
| B3 | d | d | d | d | 0 | 1 | 1 | 1 |
| B4 | d | d | d | d | 0 | 0 | 1 | 1 |
| B5 | d | d | d | d | 0 | 1 | 1 | 0 |
| B6 | d | d | d | d | 0 | 0 | 1 | 0 |
| B7 | d | d | d | d | 1 | 1 | 0 | 1 |
| B8 | d | d | d | d | 1 | 0 | 0 | 1 |
| EB | d | d | d | d | 0 | 0 | 0 | 0 |

1は押下した状態。0は押下していない状態。dはどうでもいい(ドント・ケアの意味)。  

なお、以下のツイートから上キーを押しっぱなしにすることが明らかになった。
https://twitter.com/wakamesoba98/status/1208039666076680193

つまり、マスコンを5に入れたいなら、上ボタン, 下ボタン, △ボタン, R1ボタン, R2ボタン, L2ボタンを同時押しすれば良い。

エミュレータのキーマッピングが以下の通りになっていることを前提として、コードを書いてみる。
![](https://i.gyazo.com/4dc27cdf87972763b63fa7821521490c.png)

```python
 class PS1DengoKeyPresser:
     def __init__(self):
         self.NOTCH2BITS = {
             "P5": 0b01011011,
             "P4": 0b11001011,
             "P3": 0b11011011,
             "P2": 0b01101011,
             "P1": 0b01111011,
             "N": 0b11101011,
             "B1": 0b11101110,
             "B2": 0b11101010,
             "B3": 0b11100111,
             "B4": 0b11100011,
             "B5": 0b11100110,
             "B6": 0b11100010,
             "B7": 0b11101101,
             "B8": 0b11101001,
             "EB": 0b11100000,
         }
         self.BIT2KEY = {
             0: "2",
             1: "0",
             2: "1",
             3: "9",
             4: "i",
             5: "d",
             6: "s",
             7: "a",
         }
 
    def press(self, notch):
        bits = self.NOTCH2BITS[notch]
        pyautogui.keyDown("w")
        print(notch)
        for i in range(8):
            if bits & (1 << i):
                pyautogui.keyDown(self.BIT2KEY[i])
            else:
                pyautogui.keyUp(self.BIT2KEY[i])
```


入力モジュールからnotchの情報をpress関数に渡すと、対応した「複雑なコマンド」を入力するキー入力が実行される。

# ボタン動作を追加
https://qiita.com/10_tenk/items/f21203d7c55fd3b19d62
これを参考にする。  
マッピングは以下の通り。

| button | idx | key |
| --- | --- | --- |
| a | 0 | l |
| b | 1 | k |
| y | 3 | j |
| - | 4 | b |
| + | 6 | n |

Zuikiコントローラーによるボタン入力がidx(数字)で得られるので、それをキーボード入力に変換する。

# 実装
ぶっちゃけ、実装した結果は以下のコードを見てもらえればよい。
https://github.com/sonarAIT/ZuikiController2PS1DenshaDeGo/blob/main/main.py

これだけだとあんまりなので、各モジュールについて必要最低限の解説を書く。

- `ZuikiControllerInputGetter`
    - Zuikiのコントローラーから入力を受け取り、現在のノッチ、ボタン入力を取得する。
    - `getNotch`
        - 現在のノッチを取得し、P5, EBのような形式でreturnする。
    - `getButtons`
        - 現在のボタン入力を取得し、ボタンのidxでreturnする。
- `Zuiki2PS1Dengo`
    - Zuikiのノッチ・ボタン入力を、PS1の電車でGO専用の「複雑なコマンド」に変換する。
    - `getKeys`
        - P5, EBなどのZuikiコントローラーによるノッチ入力とボタン入力から、対応するキーボード入力をreturnする。押すキーと、離すキーをそれぞれ求める。
- `PS1DengoKeyPresser`
    - `Zuiki2PS1Dengo`で得たキーボード入力を、実際に実行する。
    - `input`
        - この関数から、押すキーと離すキーを求める。あんまりキー入力を連打するとエミュレーターが壊れる(気がする)ので、「コントローラー入力の直後に0.025秒待って、新しい入力が来なかったら、実際にエミュレータに対してキーボード入力を流す。」ということをしている。
    - `press`
        - 押す。

# テスト

![](https://gyazo.com/c54803e30c160f06bff4f5ea634123e6.png)![](https://gyazo.com/ca9ab570cf15c8e622b7282ba74e4160.png)
上がプログラム実行中の様子で、下がプログラムを実行していない状態。大体狙い通りに動いた。
また、Windowsにおけるキーボードの設定において、Microsoft IME(日本語+英語)ではなくUSキーボード(英語のみ)を使用することで、レスポンスが格段に向上した。ぶっちゃけ、理由は不明だが、少なくとも間違いなさそうだ。