電Go!! の ZUIKI コンは製造時期による違いがある?

Pocket

電車でGO!!専用ワンハンドルコントローラー - 瑞起 ZUIKI (以下、 ZUIKIコン) には、 型番 : ZKNS-001 の通常版 と、 型番:‎ZKNS-002 の EXCLUSIVE EDITION の二種類ある。

ただどうも、それ以外に製造時期による違いもあるようだ。

その違いのせいで、 JR東日本トレインシミュレータ で ZUIKIコン 使うツールがな機能しなくなって困ったので、その対処をした話。

きっかけ

元々、2021年の発売当初から通常版の ZUIKIコン を購入しており、公式で対応している 電車でGo!! Switch 版 や ソニックパワードの 鉄道にっぽん! シリーズ Real Pro など に加え、 Switch版電車でGOワンハンドルマスコンで操作できるようにしてみました - Steam コミュニティー で紹介されてるツールを使って JR東トレシム で使っていたりしていた。

程よい重量感や操作感で臨場感が爆上がり… というか、無いと面白みが8割減レベルに気に入っており、最近 EXCLUSIVE EDITION を買い増した。

…ところが買い増しした ZKNS-002 は、Switch 版や 鉄道にっぽん! シリーズでは問題なく動くのに、上記ツールを使って JR東トレシム しようとすると以下のようなエラーが出て動かない。

Traceback (most recent call last):
  File "main.py", line 24, in <module>
  File "SwitchDenGo.py", line 32, in loadStatus
pygame.error: Invalid joystick button
[33064] Failed to execute script 'main' due to unhandled exception!

存在しない joystick 番号を指定されてエラーになってる。
…つまりボタン配置が違う?

しかしそもそも、この ZUIKIコン は汎用 USB コントローラ として認識されるモノだし、Windows OS 側のデバイスの設定を見ても、どちらも同じ "One Handle MasCon for Nintendo Switch" と認識され、全くボタン配置で認識されている。
なんでボタン配置がズレるのか、訳がわからないよ。

上記 Steam コミュニティー でも、同じような問題に遭遇している人が何人かいる。
しかも、 通常版 ZKNS-001 か EXCLUSIVE EDITION ZKNS-002 かを問わず発生するようだ。

つまり、型番違いによる仕様変更が原因ではない…?

意外なる原因

せっかく、エラーが出るデバイスと出ないデバイスが揃っているので、もう少し深く掘って調べてみる。

このツール mipsparc/JRESim_Dengo のソースを取って Python のデバックをしてみたところ、やはりというか pygame というライブラリでコントローラーを操作するあたりでエラーがでている。

031:        # ○ボタン
032:        if self.joy.get_button(15):  # <- ここでエラー
033:            self.buttons.append("SW_CIRCLE")
034:        # HOMEボタン
035:        if self.joy.get_button(5):
036:            self.buttons.append("SW_HOME")

試しに、 pygame のジョイスティック実行例コード joystick.py を実行して、 pygame 側でどのように認識されているか確認してみる。

んん゙っ!?
ZKNS-002 のほうは "One Handle MasCon for Nintendo Switch Exclusive Edition" として認識されるものの、
発売日に買った方の ZKNS-001 が "Nintendo Switch Pro Controller" と認識されて、ボタンの認識軸数が大きく変わっている。

pygame は、内部的に SDL2 (libsdl-org/SDL) というライブラリでジョイスティック操作をしている。
その SDL では、既知の様々なコントローラに様々な特殊対応が為されているので、そこらへんが影響していそうだ。

こういったものは、 USB の VID (ベンダーID) と PID (プロダクトID) で場合分けしているものと相場が決まっている。
ということで、デバイスマネージャーのプロパティ(ハードウェアID)から VID と PID を確認してみる。

  • 発売日購入の通常版 ZKNS-001:
    • HID\VID_0F0D&PID_00C1&REV_0106
  • 最近買った EXCLUSIVE EDITION ZKNS-002:
    • HID\VID_33DD&PID_0002&REV_0111

VID ちゃうやんけ。

https://www.usb.org/developers で USB ベンダーID を検索してみると…
0x0F0D (3853): HORI CO., LTD.
0x33DD (13277): ZUIKI Inc.

?? HORI ??

とりあえず、上記の VID と PID をもとに、 SDL2 (SDL 2.0.18) でどのように動作が定義されているか調べてみる。

SDL/src/joystick/controller_type.h at release-2.0.18 · libsdl-org/SDL

    { MAKE_CONTROLLER_ID( 0x0f0d, 0x00c1 ), k_eControllerType_SwitchInputOnlyController, NULL },  // HORIPAD for Nintendo Switch

ホリパッド for Nintendo Switch って書いてあるぞ。

更にここら辺 (SDL/src/joystick/SDL_joystick.c) の処理を経て、 VID: 0x0F0D & PID: 0x00C1 の組み合わせのものが "Nintendo Switch Pro Controller" と認識されているようだ。

なお、 VID: 0x33DD & PID: 0x0002 については、特に SDL 側での特殊処理は定義されていなそうだった。
このため、 USB H/W 側で定義されている "One Handle MasCon for Nintendo Switch Exclusive Edition" の名前がそのまま pygame での認識名として表示されていたのだろう。

うーん、製造当初は適当に(Switch に対応した)ホリパッドを偽装してたのを、後のロットではちゃんと VID や PID とって設定したってコトかしら…?
ライセンス品としてどうなんだソレ。

とりあえず、以下の2点が今回の問題が発生していることがわかった。

  1. 製造時期?によって、ホリパッドを偽装しているものとそうでないものがある
  2. ホリパッドは SDL 側に特殊対応があり、偽装の有無でライブラリからとれるキー配置が異なっている

JRESim_Dengo の修正

原因がわかったところで、 JRESim_Dengo の方をどちらの ZUIKIコン にも対応させたい。

とはいえ、 VID や PID で動作し分けるにしても、私が持っている2台とは別の VID や PID を持つものもありそうだ。
(製造時期の違いからか同じ不具合に遭遇していた、通常版の ZKNS-001 とか)

SDL が動作を書き換えなければ、 USB から出ている信号は同じボタン配置で出ているっぽい為、コントローラーの軸数やボタン数で SDL 側での書き換えが行われたか判別すればよさそうだ。

--- ./SwitchDenGo_old.py    2022-10-03 18:55:53 +0900
+++ ./SwitchDenGo.py        2023-10-08 05:51:32 +0900
@@ -7,36 +7,64 @@

     def __init__(self):
         pygame.init()
         self.joy = pygame.joystick.Joystick(0)
+        self.ctrl_nums = (self.joy.get_numaxes(), self.joy.get_numballs(), self.joy.get_numbuttons(), self.joy.get_numhats())
+        if self.ctrl_nums not in [(6, 0, 16, 0), (4, 0, 14, 1)]:
+            raise Exception("サポートされていないコントローラです")
         self.joy.init()

     def loadStatus(self):
         self.brake_knotch = 0
         self.accel_knotch = 0
         self.buttons = []
         pygame.event.get()

-        # Xボタン
-        if self.joy.get_button(2):
-            self.buttons.append("SW_X")
-        # Yボタン
-        if self.joy.get_button(3):
-            self.buttons.append("SW_Y")
-        # Aボタン
-        if self.joy.get_button(0):
-            self.buttons.append("SW_A")
-        # Bボタン
-        if self.joy.get_button(1):
-            self.buttons.append("SW_B")
-        # ○ボタン
-        if self.joy.get_button(15):
-            self.buttons.append("SW_CIRCLE")
-        # HOMEボタン
-        if self.joy.get_button(5):
-            self.buttons.append("SW_HOME")
-
-        knotch_level = self.joy.get_axis(1)
+        # ロンチ版
+        if self.ctrl_nums == (6, 0, 16, 0):
+            # Xボタン
+            if self.joy.get_button(2):
+                self.buttons.append("SW_X")
+            # Yボタン
+            if self.joy.get_button(3):
+                self.buttons.append("SW_Y")
+            # Aボタン
+            if self.joy.get_button(0):
+                self.buttons.append("SW_A")
+            # Bボタン
+            if self.joy.get_button(1):
+                self.buttons.append("SW_B")
+            # ○ボタン
+            if self.joy.get_button(15):
+                self.buttons.append("SW_CIRCLE")
+            # HOMEボタン
+            if self.joy.get_button(5):
+                self.buttons.append("SW_HOME")
+
+            knotch_level = self.joy.get_axis(1)
+        elif self.ctrl_nums == (4, 0, 14, 1):
+            # Xボタン
+            if self.joy.get_button(3):
+                self.buttons.append("SW_X")
+            # Yボタン
+            if self.joy.get_button(0):
+                self.buttons.append("SW_Y")
+            # Aボタン
+            if self.joy.get_button(2):
+                self.buttons.append("SW_A")
+            # Bボタン
+            if self.joy.get_button(1):
+                self.buttons.append("SW_B")
+            # ○ボタン
+            if self.joy.get_button(13):
+                self.buttons.append("SW_CIRCLE")
+            # HOMEボタン
+            if self.joy.get_button(12):
+                self.buttons.append("SW_HOME")
+
+            knotch_level = self.joy.get_axis(1)
+        else:
+            raise Exception("サポートされていないコントローラです")

         if knotch_level > 0.95:
             self.accel_knotch = 5
         elif knotch_level > 0.75:

こんな感じの修正を入れてビルドしたものを以下のリリースで展開した。

もし、最近 ZUIKIコン を JREトレシム で使おうと思って困っている人がいればどうぞ。

元の作者の方にもプルリク投げとくか。

補足

実装のために調査した、各環境・デバイス毎の、キー割り当ては以下の通りだった。

DirectInput からの取得 (ロンチ版, 最近の Exclusive Edition 共通):

index 1軸: マスコン
Button 1: Y
Button 2: B
Button 3: A
Button 4: X
Button 5: L
Button 6: R
Button 7: ZL, EB
Button 8: ZR
Button 9: -
Button 10: +
Button 11: N/A
Button 12: N/A
Button 13: HOME
Button 14: Capture
PoVハット 1: 方向キー

pygame 経由の、ロンチ版

index 1軸: マスコン
index 4軸: ZL
index 5軸: ZR
Button 0: A
Button 1: B
Button 2: X
Button 3: Y
Button 4: -
Button 5: Home
Button 6: +
Button 7: N/A
Button 8: N/A
Button 9: L
Button 10: R
Button 11: Up
Button 12: Left
Button 13: Down
Button 14: Right
Button 15: Capture

# ハードウェアID: 'HID\VID_0F0D&PID_00C1&REV_0106'
joy = pygame.joystick.Joystick(0)
joy.get_guid()
# -> '030000000d0f0000c100000006016800'
joy.get_name()
# -> 'Nintendo Switch Pro Controller'
joy.get_numaxes()
# -> 6
joy.get_numballs()
# -> 0
joy.get_numbuttons()
# -> 16
joy.get_numhats()
# -> 0

pygame 経由の、 Exclusive Edition

index 1軸: マスコン
Button 0: Y
Button 1: B
Button 2: A
Button 3: X
Button 4: L
Button 5: R
Button 6: ZL
Button 7: ZR
Button 8: -
Button 9: +
Button 10: N/A
Button 11: N/A
Button 12: Home
Button 13: Capture
PoVハット 1: 方向キー

# ハードウェアID: 'HID\VID_33DD&PID_0002&REV_0111' 
joy = pygame.joystick.Joystick(0)
joy.get_guid()
# -> '03000000dd3300000200000000000000'
joy.get_name()
# -> 'One Handle MasCon for Nintendo Switch Exclusive Edition'
joy.get_numaxes()
# -> 4
joy.get_numballs()
# -> 0
joy.get_numbuttons()
# -> 14
joy.get_numhats()
# -> 1

電Go!! の ZUIKI コンは製造時期による違いがある?」への4件のフィードバック

  1. 詳細に調べていただき、ありがとうございます!!
    PRまで頂いて申し訳ないですが、多忙なためEXEファイルにビルドする時間が取れないかもしれません…フォークして、そちらをご案内していただけると大変助かります。

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください