Doloc TownにXUnity.AutoTranslatorを導入したらショートカットキーが効かなかったので、AIにplugin書かせて対処した記録

ChatGPT o3くんと一緒に土曜の午前中を使って対処に勤しんだ記録

成果物

github.com

前置き

Doloc Townはいいぞ

store.steampowered.com

Doloc Town + BepInEx 5.4.23.3 + XUnity.AutoTranslator 5.4.5 の組み合わせでAutoTranslatorのショートカットキーが動作しなくて困っている人へ

  • GitHub ReleaseからdllをダウンロードしてBepInEx/pluginsにぶち込んでね
    • 確認はしてないけどたぶんBepInEx 5.4.x以降なら動くよ
    • 心配ならソースから自分でビルドしてね
  • ALT+0, ALT+1で開くUIはこれ入れても動作しないよ
    • ALT+R (翻訳ファイルリロード)と ALT+T (翻訳のトグル) が動けば良い人向け
  • 以降は読む必要ないよ

この問題の技術的詳細が知りたい人へ

  • ほぼ全部AIによろしくしているので、嘘を書いていても責任は取りません
  • 筆者はウェッブアプリケーッションエンジニャーでありUnityのことは一切何も知らない

この解決法の応用範囲

  • 該当versionのBepInExとAutoTranslatorを利用しており、ENABLE_LEGACY_INPUT_MANAGER=false でビルドされたUnity製のゲームの組み合わせでたぶん同じことが起こる
  • それにも適用できると思われる

本題

起きていた事象

  • Doloc TownにBepInExとXUnity.AutoTranslatorを導入した
    • 翻訳機能自体は効いているが、翻訳ファイルを再読み込みしたり翻訳をトグルするキーが効かない
  • ログには以下のように出ている
  [Warning:XUnity.AutoTranslator] Input API is not available!
  System.InvalidOperationException: You are trying to read Input using the UnityEngine.Input class, but you have switched active Input handling to Input System package in Player Settings.
    at (wrapper managed-to-native) UnityEngine.Input.GetKeyInt(UnityEngine.KeyCode)
    at UnityEngine.Input.GetKey (UnityEngine.KeyCode key) [0x00001] in <6a4057336492450e9285cede3e4bf821>:0 
    at XUnity.AutoTranslator.Plugin.Core.AutoTranslationPlugin.HandleInput () [0x00000] in <cd27375817094ee0902fac53c283e1a5>:0 
    at XUnity.AutoTranslator.Plugin.Core.AutoTranslationPlugin.HandleInputSafe () [0x00007] in <cd27375817094ee0902fac53c283e1a5>:0 

ChatGPT o3に聞いてみた

BepInEx 5.4.23.3 + XUnity.AutoTranslator 5.4.5 をDoloc Townというゲームに適用しています。
機能は働くんですが、AutoTranslatorに備わっているショートカットキーが効きません。
以下のようなログが出ています。BepInExを6.xに変えたりAutoTranslatorのversionを変更したりは既に試しました。
ゲーム側で古いInput APIが無効化されており、BepInExかAutoToranslatorのどちらかがこれに対応していないというのが原因だと思いますが、これを動かせるようにする方法を教えてください

原因

原因は「ゲーム本体が Active Input Handling を Input System (New) のみでビルドしているため、
UnityEngine.Input.* を呼ぶ XUnity.AutoTranslator のホットキー処理が例外を出して停止している」ことです 
BepInEx 5.4 以降は新旧どちらの入力にも対応していますが、AutoTranslator 側は旧 API 固定のままです 

対処1(失敗)

  • …/DolocTown_Data/globalgamemanagers を開いて PlayerSettings の activeInputHandler を2にセットすればいいよ
  • これで 旧 Input クラスが再び有効になりますよ^^
  • と言われたので UnityPy を使って書き換えるコードを書かせて実行
    • しかし既に activeInputHandler は2になっていた

対処1の結果をAIに投げた

Doloc Town はビルド時に レガシー Input API を完全に除去 しており、
UnityEngine.Input.* を呼ぶと例外を投げる設定(ENABLE_LEGACY_INPUT_MANAGER 定義が false)でコンパイルされています。
そのため PlayerSettings を書き換えても旧 API は復活しません。
  • そうなんだ(無知)
前回ご案内した 方法②(旧 Input 呼び出しを新 Input System に橋渡しする BepInEx プラグイン)が唯一の現実的手段になります
  • とのことなので、ビルド環境のお膳立てからなにからしてもらいBepInEx pluginを書いた
    • Unity本体が入ってないとダメなのかと思って環境構築めんどくせぇなと思ってたけど、dllさえありゃビルドできるんですね(無知)
  • 言う通りにしたらビルドが失敗する、というのが5回くらい続いたが、ログ貼ってデバッグしてもらいつつ最終的には解決

ビルドの結果生まれたdllを読み込んだがやはりキーが効かない

  • ログを貼ったらしっかりデバッグしてくれた
InputShim の Prefix が AutoTranslator のパッチより後に呼ばれてしまい,
結局 Input.GetKeyInt が実行されて例外→ホットキー無効 という状況です。
(BepInEx はプラグイン読み込み順=パッチ登録順なので,あとからロードした InputShim の Prefix が 最初 に動くと思いがちですが,Harmony は 同一 Priority の場合 ID がアルファベット順で早いものが先に実行 というルールです。
XUnity.AutoTranslator → jp.example.inputshim の順になり,InputShim の Prefix が後回しになっています)
  • そうなんだ(無知)
  • こういう仕組みである以上複数プラグインの動作の整合性取るのクッソ面倒そう

XUnity.AutoTranslatorにcontributeしとこうかなーと思って難易度を聞いた

「プルリク 1 本で済む」ほど簡単ではありません。
旧 UnityEngine.Input 依存は キー入力 だけではなく マウス/スクロール/タッチ でも広範に使われています。
BepInEx 6 系以降は 新旧 Input API を抽象化するヘルパー(BepInEx.Unity.Input 名前空間)が用意されていますが、AutoTranslator 本体は 5 系依存で止まっており、直接は利用していません。
やるなら
依存 DLL を最新版へ更新(libs/ を丸ごと入れ替え)。
HandleInput・GUIUtil を中心に Input ラッパー層を挟む。
Mono/IL2CPP 両ターゲット・Unity 2017〜2023 で回帰テスト。
既存ホットキー CFG 互換・マクロ録画系ツールとの併用確認。
という“中規模 refactor”規模になります。
  • じゃあ・・・やめとくか・・・Unityのことなんも知らんし・・・

感想

  • 何も知らない分野でも2時間かそこらで問題解決できたのでAIってすごいなと思いました(小学生並みの感想)
  • GitHubで公開してブログも書くか!とかやったので結局半日溶けてしまった

Pixel 3a XLからPixel 6aに移行した

Pixel 3a XLのサポート切れてたことに気付いたので6aを買って移行した

基本的にはケーブルでつないでボタンポチーするだけだったが、アプリごとに追加の対応が必要だったものをまとめておく

移行

付属のケーブルに両方の端末を繋げて画面の指示に従えばだいたい移行完了

ホーム画面等もそのまま移行してくれる

アプリごと個別の対応

モバイル Suica

www.jreast.co.jp

LINE

新しい端末でログインしたら画面の指示に従えばOK

過去のトークも含め全部移行できる

Authy

以下はSettings -> Accounts -> BackupsはONにしている前提

電話番号を入れてSMS認証したら移行完了

移行はされているが内容は暗号化されているので、復号化パスワードを入力しアンロック

Google Pay

iDを使っているのでそれを移行

作業前に古い方で、カードを選択し右上のメニューから「非接触型決済を無効にする」を実行しておく

カード情報は移行されていたので、再入力非接触型決済を有効にするだけ

ここでCVCやカード名義などの情報は再入力が必要だった

OMRON connect

新しい方でアプリを立ち上げると初回設定時のプロンプトが現れるのでそれに従う

個人番号を上書きしようとすると古い端末の連携を解除しとけと勧められるので、それに従って解除後新しい方で連携

続いてクラウドサービスにログインし、すべての過去データ復元

ゆうちょ認証アプリ

www.jp-bank.japanpost.jp

Fitbit

climbmountain.jp

ログインしたら一通り過去データは引き継がれるが、一度古い端末で解除しないと新しい端末でペアリングできない

解除時は「アカウントから削除しますか?」という文面だが、既にfitbit accountに同期済みのデータは失われることはない

解除したら新しい端末で新規に機器を追加すれば完了

Fit to Fit

何も設定しなくてもsyncされてた

Coke ON

特にログインして使ってなかったので、メニューの「機種変更、データ移行・復元」からアプリID・PINを利用して移行

移行後は前の端末ではアプリを利用できなくなる

支払い設定は(たぶん)引き継がれないので再設定が必要

Meta Quest 2

新しい端末でログイン後、Quest 2本体の電源を入れたら勝手にペアリングされる

古い端末の方はログインセッション切れでアプリからログアウトされる

同時に使えるかどうかは知らぬ

Sony Headphones

特に引継ぎはされてないっぽい

初回セットアップっぽいのをこなしてサインインし、使用している音響機器を再度登録していく作業が必要

WF-1000XM4とWH-1000XM3を再登録。WF-1000XM3も持ってるけど充電できなくなっちゃったのでお前は捨て置く

Soundcore

特に引継ぎはされてなさそう。ログインして機器を接続し直した

HearIDの設定は残ってたけどこれはイヤホン本体に保存されてたっぽいね

WF-1000XM4は問題なかったけど、WH-1000XM3とSoundcore Space A40は接続済みのデバイス一覧で「接続エラーです。デバイスをOFFにしてからONに戻してください」という表示が出る。その通りにしても直らない

しかし問題なく接続はされていてアプリで認識もされてるし音も聞こえる。謎

Steam

ログインすると古い端末の方に認証リクエストが飛ぶので、それを認証すればOK

ただしSteam Guardの機能はそのまま古い端末側にあるので、Steam Guard -> Move Authenticatorで新しい端末の方へ移行しておく

GO タクシー

アカウント登録せずに使っていたので、古い端末でアカウント登録した上で新しい端末でログインする必要があった

GO Payの登録情報はそのまま引き継がれる様子

ニコニコ動画

新しい端末でアプリ開いたら既にログインされてた

セッションまで引継ぎしなくていいぞ

以上

終わり

昼過ぎに端末届いてこれ書いてる時間を除いて2時間半くらいはかかった

クレジットカード更新時や電話番号変わったときほどではないけど移行作業めんどくさいね、アプリ再インストールしなくていいしだいぶ楽ではあるんだろうけど

PySideで動くもの作るまでに詰まったところのメモ

環境

  • windows7 64bit
  • Python 3.3.2 (32bit)
  • cx_Freeze 4.3.2
  • pip 1.5.4
  • PySide 1.2.1

パス

以降python

C:\python33

にインストールしたものとする

PySideのインストール

> pip install PySide

おわり

QtDesignerってどこにあるの

  • PySideインストールしたら一緒についてきます
C:\python33\Lib\site-packages\PySide\designer.exe

QtDesignerで作ったuiファイルをpyに変換

> pyside-uic app.ui -o app.py

すると「予期せぬエラーでたし中断するわ^^ すまんな^^;」と言われるのでランボー怒りのデバッグ出力

> pyside-uic -d app.ui -o app.py

すると

  # ... 略
  File "C:\python33\Lib\site-packages\pysideuic\uiparser.py", line 212, in createWidget
  bg_name = bg_i18n.string
AttributeError: 'str' object has no attribute 'string'

ということなので

C:\python33\Lib\site-packages\pysideuic\uiparser.py

の該当行から.stringを除いてやれば動く
参考:Bug 1156 – PySide 1.1.0 compile error when assigning radio buttons into a button group

PySideのドキュメント

ここ → http://pyside.github.io/docs/pyside/

Qt関係は正直ググったらStack Overflowがこれでもかって程ヒットするのであまり困らなかった

  • QTableViewでセルをクリックしたら一行すべて選択したい
QTableView.setSelectionBehavior(QAbstractItemView.SelectRows)
  • QTableViewで複数行選択できないようにしたい
QTableView.setSelectionMode(QAbstractItemView.SingleSelection)
  • QTableViewでスクロールがなんかガックガクなのですっきりなめらかにしたい
QTableView.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
QTableView.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)

そうだexeにしよう

> cxfreeze app.py --target-dir dist --base-name Win32GUI --include-modules atexit

参考:Packaging PySide applications on Windows - Qt Wiki

できました

できあがったexeはdllやpydを含めて31MBほど
ひえーでっかい

compile終了時にキュピーンと音を鳴らしてみる

今書いてるプロジェクトがコンパイルに1分程度かかるようになってきたので、コンパイル終了時に音で知らせてくれたら良くね?とふと思って書いた。
コンパイルの成功/失敗が音で分かるようになります。
ただし使ってると段々うっとうしくなってきます。


sbt0.13でmapRがdeprecatedになってたので修正 (2013/12/20追記)

環境
コード
  • build.sbt
// http://www.scala-sbt.org/release/docs/Detailed-Topics/Tasks.html#handling-failure

//compile in Compile <<= compile in Compile mapR {
compile in Compile <<= (compile in Compile result) map {
  case Inc(inc: Incomplete) =>
    Sound.play("sound/NG.wav")
    throw inc
  case Value(v) =>
    Sound.play("sound/OK.wav")
    v
}
import java.io.File
import javax.sound.sampled._

import scala.concurrent.ops._

// 参考
// http://www.ibm.com/developerworks/jp/java/library/j-5things12/#N101D9

object Sound {

  def play(file: String) {
    spawn {
      val clip = AudioSystem.getClip()
      clip.addLineListener(new LineListener(){
        def update(e: LineEvent) {
          if (e.getType == LineEvent.Type.STOP) {
            clip.synchronized { clip.notify() }
          }
        }
      })
      val soundfile = new File(file)
      val stream = AudioSystem.getAudioInputStream(soundfile)
      clip.open(stream)
      clip.start()
      clip.synchronized {
        clip.wait()
      }
      clip.drain()
      clip.close()
      stream.close()
    }
  }
}

waveファイルは好きなものを自前で用意して好きな所に置いてください。
上の例ではprojectのrootにsoundディレクトリを作ってそこにぶち込みました。
あまり長い音声を設定すると、前の音声が終わらないうちに次のが重なって鳴り始めてひどいことになります。


sbt0.12系はまだscala2.9なのでconcurrent.ops.spwanを使ってますが、sbt0.13からはscala2.10でfuture使えるためsbt0.13-SNAPSHOTを使ってる人はfutureで書いた方がいいです(2.10だとconcurrent.opsは丸ごとdeprecated)

scala2.10.0-RC1でscala.actorsパッケージがscala-library.jarから追い出されている

ので、2.10.0-RC1でscalaのactorを使いたい時はbuild.sbtに

scalaVersion := "2.10.0-RC1"

libraryDependencies <+= scalaVersion { 
   "org.scala-lang" % "scala-actors" % _
}
//libraryDependencies <+= scalaVersion { "org.scala-lang" % "scala-reflect" % _ }

と依存を追加すればOK。


直接は使ってなかったけどscalatestがscala.actor使ってたので、test実行したら

NoClassDefFoundError: scala/actors/Actor$

って言われてびびった。


ここ見る限り2.10.0-M3からjar別になったっぽい。
上に書いたけど、scala.reflectも同じくハブられてるので注意。


環境:
scala : 2.10.0-RC1
sbt : 0.12.0
scalatest : 2.0.M4

参考:
ScalaTest IDE plug-in and 2.10 - scalatest-users
Scala2.10 以降に入るかもしれない機能まとめ - xuwei-k's blog

sbtでネイティブライブラリを使う時のメモ

2012/11/02追記

もはやうろ覚えなのですが、このエントリを書いた頃は確かsbt0.7が最新だったはずなので以下は超絶古い情報です。
もう今はsbt0.13が出そうな状況だというのに「sbt dll」とかでググると未だにこのエントリが割と上の方に出てきやがることに危機感を覚えたので追記しておきます。


This page has moved
scala - Integrating native system libraries with SBT - Stack Overflow
↑を読んだらいいと思います!!以上!!!

以下はsbt0.7の情報

ググってもほとんど情報が見つからなかったのでメモ。


プロジェクトのルートディレクトリにネイティブライブラリ(dll,so)を置いてしまうと、二度目以降のrun時に

> java.lang.UnsatisfiedLinkError: Native Library ***.dll already loaded in another classloader

と出て実行できなくなります。


sbtでは、アプリケーションは通常sbtと同じJVM上で実行される*1みたいです。sbtのソースを見るとrunする時に新しいクラスローダを作ってるので、1度目のrunでdllをロードした古いクラスローダが生き残ってて冒頭のエラーが出るのかな。ごめんなさい検証せずに適当書いてます。


これを回避するにはforkRunを使ってアプリケーションをsbtとは別のJVMで起動し、JVMオプションのjava.library.pathでdllの場所を指定してやればいいです。
ルートディレクトリ以外の適当な場所にディレクトリを作って、dllをぶちこみます。ここではlib/nativeにしておきましょう。
project/build/Project.scala のプロジェクト定義の中に以下を記述。

def nativeLibPath = System.getProperty("path.separator") + (path("lib") / "native" )

override def fork = forkRun(
  ("-Djava.library.path=" + System.getProperty("java.library.path") + nativeLibPath) :: Nil
)

もういくらrunrunしてもUnsatisfiedLinkErrorが出ることはありません。めでたい。
これに気づかずしばらくの間、一度runしたらいちいちsbtを起動し直すというあほなことを続けてました……


参考:
Google Code Archive - Long-term storage for Google Code Project Hosting.
Scala and Sbt getting started.