[IPSwitch] 誰でもできるコード開発 #6

DebugMenu

今回はDebugMenuの移植をやりたいと思います。

DebugMenuについてはSplatoon2のwikiが詳しいのでそこから抜粋。

A debug menu in a modern Nintendo game!? BOOYAH! This debug menu exists in all versions of Splatoon 2 from 1.0.0 to 3.2.0, including the Global Testfire and the Splatfest World Premiere demos. Someone tipped Nintendo off about its existence in retail copies, so they removed it from the game in an update. However, we can still play around with it in the demos!

デバッグメニューが最近の任天堂のゲームにあるだって!?やったね!このデバッグメニューは先行試射会や前夜祭を含む1.0.0から3.2.0までの全てのスプラトゥーン2のバージョンに存在します。誰かがこっそりと製品版でのデバッグメニューの存在に気づいて情報を得たので、任天堂はアップデートでゲームからデバッグメニューを削除しました。ただし、デモ版ではまだ遊ぶことができるよ。

先行試射会と前夜祭のDebug PatchはOatmealDome氏が公開しています。

全部で300行くらいあって移植は大変そうな感じがするのですが、後半部分はDebug Menuを日本語から英語に翻訳するコードなので後半の100行はほとんど無視できます。

つまり、200行ほど移植すればいいことになります。これでも結構苦労する作業ですが、3.1.0以前であればサブルーチン名がそのまま残っているので検索は容易です。

DebugMenu(1.0.0)

1.0.0向けのDebugMenuはkhang06(@Khangarood)氏がリリースしているのですが、タイトルがbad dbgsetting patch v2となっており何やら怪しい雰囲気が漂っていますね。

// DbgSetting over ShootingRange
@enabled
01455850 540F1A00
001A0F84 E0FF8152
001A0F94 ABAD2594 
0120828C 82030014
0120748C 16000014
01207490 CF000014
012074B4 18010014
012074B8 55010014
012074E0 57020014
012074BC 0E020014
012AF368 E0031FAA
012AF36C 1BFFFF17

すごく簡単な解説

最初から順番に解説していきましょう。

01455850 540F1A00

試し撃ち場をDebugMenuに置換するコードです。

001A0F84 E0FF8152

オペレータIDを開発者モードに切り替えるコードです。

001A0F94 ABAD2594

Cstm::newScene<TitleForShow::Scene>()で遷移する先をTitleForShowからDebugMenuに変更するコードです。

019BCCEC 82030014
019BBEEC 16000014
019BBEF0 CF000014
019BBF14 18010014
019BBF18 55010014
019BBF40 57020014
019BBF1C 0E020014

DebugMenuのテキストを表示するコードです。

01A661A4 E0031FAA
01A661A8 1BFFFF17

純正コントローラではDebugMenuでコントローラが効かないので、それを修正します。

DebugMenu(3.1.0)

今回はKhang06氏の不完全なコードではなく、OatmealDome氏の前夜祭向けのコードを移植することにします。

赤い色がついているのはKhang06氏のコードでは移植されていないものです。

_ZN3Cmn3Def12isDevelopSeqEv

デバッグメニューを起動するためのフラグ管理で、これが常にTrueを返すように変更します。

0を代入するコードになっているので、1を代入するコードに変更します。

// MOV W0, WZR -> MOV W0, #0x1
// Cmn::Def::isDevelopSeq() always returns true
@enabled
0009CF44 20008052

_ZN3Cmn8SoundMgr10initializeEPN4sead4HeapES3_

Lan ModeでクラッシュしないようにCmn:SoundMicを初期化しないように変更します。

Cmn::SoundMic()を初期化するところをNOPで呼び出されないようにします。

// BL #0x8438 -> NOP
// Don't initialize Cmn::SoundMic
@enabled
001C5BC8 1F2003D5

ZN4Cstm8newSceneIN4Shop5SceneEEEPN2Lp3Sys5SceneEiPN4sead4HeapE

新しいシーンの読み込みをデバッグメニュー起動に上書きします。

OatmealDome氏のコードではShopをDebugMenuに上書きしており、Khang06氏のコードではShootingRangeをDebugMenuに上書きしています。

このサブルーチンは二つ命令を置き換える必要があるので一つずつ見ていきます。

// World Premiere 0x162084 MOV W0, #0x390 -> MOV W0, #0x1000 // Splatoon 3.1.0 0x227200 MOV W0, #0x398 -> MOV W0, #0x1000

最初はよくわからない値を0x1000に置き換えます。これはもう先行試射会でも同じ値が使われていたので、この値を使えということなのでしょう。

もう一方はシーン変更ですが、Shopに飛ぶはずの命令をDbgSettingにとばせばいいだけなのでアドレスを調べて差を求めれば終わります。

3.1.0ではDebugSetting::Sceneは0x1238FD0に書かれているので、0x1238FD0 – 0x227210 = 0x1011DC0を計算してジャンプすべきアドレスを求めます。

// World Premiere 0x162084 BL #0xA4BA20 -> BL #0x97DF24 // Splatoon 3.1.0 0x227210 BL #0x10D2684 -> BL #0x1011DC0

あとはこれらをHEXに変換すれば終わりです。

// DbgSetting in Cstm::newScene<Shop::Scene>(sead::Heap*)
@enabled
00227200 00008252
00227210 70474094

でもショップをひらいたらDebugMenuが起動するってめんどくさくない?(心の声)

言われてみれば確かに…

というわけで、Boot後に即座にDebugMenuがひらくようにしてみます。

これは<Shop::Scene>ではなく<TitleForShow::Scene>に対して命令を上書きすれば良いので、以下のようになります。

// DbgSetting in Cstm::newScene<TitleForShow::Scene>(sead::Heap*)
@enabled
-------- E0FF8152
-------- --------

ここはみなさんへの宿題ということで。

_ZN4Cstm8newSceneIN13ThanksForShow5SceneEEEPN2Lp3Sys5SceneEiPN4sead4HeapE

ダミーマッチは自身のシーン関数を持たないのでThanksForShowのnewSceneを書き換えます。

// DummyMatch in Cstm::newScene<ThanksForShow::Scene>(sead::Heap*)
@enabled
00227350 00008252
00227360 07714094

ここ移植ミスってるかも…

// DummyMatch in Cstm::newScene<ThanksForShow::Scene>(sead::Heap*)
@enabled
00227350 E0FF8152
00227360 07714094

うごかなかったらこっちでどうぞ。

_ZN2Lp3Sys15cpAssertProductEi

ブート時に開発者モードを有効化します。

関数に入った瞬間にRETするように命令を変更します。

// MOV W8, W0 -> RET
// Dummy out Lp::Sys::cpAssertProduct()
@enabled
019A7E14 C0035FD6

デバッグメニューのテキスト移植

debug textとpilot textについて行いますが、めんどくさかったのでMeshi(@meshi_0217)氏のコードを参考にしました。

掲載許可をもらっていないので、ここでは紹介しません。

まあでも1.0.0向けのKhang06氏のコードを移植すればいけると思いますよ。

_ZN2Lp3Utl5Limit24getSceneDbgMixCtrl_PilotEv

デバッグコントロールMixではなく標準コントローラを呼び出します。

// SUB SP, SP, #0x10 -> MOV X0, WZR
01A661A4 E0031FAA

OatmealDome氏のコードを見るとLp::Utl::getCtrl(int)にジャンプするコードになっているのでまずはそのサブルーチンのアドレスを調べます。

すると0x1A65E14にあることがわかるので、ジャンプするためのオフセットは以下のように計算できます。

0x1A65E14 – 0x1A661A8 = 0xFFFFFFFFFFFFFC6B

このとき「前にジャンプする」となっているときはこの計算結果より1増やさないといけないので、実際の命令は以下のようになります。

// ADRP	X8, #0x26F2000 -> B #0xFFFFFFFFFFFFFC6C
01A661A4 1BFFFF17

たまたまこれは前夜祭と変わらなかったので同じコードが流用できました。

これら二つをまとめると、以下のコードが得られます。

// Redirect Lp::Utl::Limit::getSceneDbgMixCtrl_Pilot() to Lp::Utl::getCtrl()
@enabled
01A661A4 E0031FAA
01A661A8 1BFFFF17

これをやらないとコントローラで操作できないので必須です。

ここ4バイトズレててDebugMenu動かなくて二十分くらい悩んでました…

_ZN10DummyMatch5SceneC2EPN4sead4HeapE

LanModeでのダミーマッチを使うことでサーバが落ちていてもマッチングが行え、BANのリスクがないので安全です。

// B loc_1243B18 -> NOP
// MOV W8, WZR -> MOV W8, #0x1
// Force ChangeLanMatchInBoot to always be true
@enabled
01243B10 1F2003D5
01243B14 28008052

Sample/Actor -> DbgSetting

ここから急に難しくなるので注意して下さい。

書き換えたいアドレスには「aSampleActorへのアドレス」「定数0x403」「Sample/Actorへのアドレス」の三つのデータが保持されています。

このうち、定数に関しては書き換え後も変わりません。

つまり、aSampleActorへのアドレスをaDbgsettingへのアドレスに書き換えて…

Sample/ActorへのアドレスをCstm::newScene<TitleForShow>に書き換えればいいわけやね。

Cstm::newSceneは既にDbgSettingがひらくように変更してあるのでこれで動作するわけです。

3.1.0address
aSampleActor3E1FAB8
aDbgsetting3E1F820
Sample/Actor372163F
Cstm::newScene<TitleForShow::Scene>()02272CC
// 3.1.0 0x01C27E40
B8 FA E1 03 00 00 00 00  03 04 00 00 00 00 00 00
3F 16 72 03 00 00 00 00
->
20 F8 E1 03 00 00 00 00  03 04 00 00 00 00 00 00
CC 72 22 00 00 00 00 00

// Add newScene function for DbgSetting
@enabled
01C27E40 20F8E103
01C27E50 CC722200

Sample/Actor -> FreeTest

やることは先程と似たような感じです。

3.1.0address
aFreeTest3E1F538
Cstm::newScene<Game::CmnScene>()0227080

ただ、先程は上書きするアドレスが上書きしても大丈夫そうなサブルーチンだったのですが、今回は微妙に危なそうです。

// 3.1.0 0x01C27E58
C8 FA E1 03 00 00 00 00  03 04 00 00 00 00 00 00
E8 0B 71 03 00 00 00 00
->
38 F5 E1 03 00 00 00 00  03 04 00 00 00 00 00 00
80 70 22 00 00 00 00 00

// Add newScene function for FreeTest
@enabled
01C27E58 C8FAE103 
01C27E68 80702200

Sample/Actor -> DummyMatch

3.1.0address
aDummyMatch3E1F920
->
3E1F922
Cstm::newScene<ThanksForShow::Scene>()0227320

aDummyMatchのアドレスは0x3E1F920なのですがOatmealDome氏のコードを見ると2バイトズラしているのでそれに倣ってぼくも2バイトズラします。

// 3.1.0 0x01C27E70
D0 FA E1 03 00 00 00 00  03 04 00 00 00 00 00 00
4C 16 72 03 00 00 00 00
->
D0 FA E1 03 00 00 00 00  03 04 00 00 00 00 00 00
20 73 22 00 00 00 00 00

// Add newScene function for DummyMatch
@enabled
01C27E70 D0FAE103
01C27E80 20732200

完成したコード

テキストのところを紹介していないのでそのままでは動かないのですが、80%くらい完成したものを公開しておきます。

// Cmn::Def::isDevelopSeq() always returns true
@enabled
0009CF44 20008052

// Don't initialize Cmn::SoundMic
@enabled
001C5BC8 1F2003D5

// DbgSetting in Cstm::newScene<Shop::Scene>(sead::Heap*)
@disabled
00227200 00008252
00227210 70474094

// DummyMatch in Cstm::newScene<ThanksForShow::Scene>(sead::Heap*)
@enabled
00227350 E0FF8152
00227360 07714094

// Dummy out Lp::Sys::cpAssertProduct()
@enabled
019A7E14 C0035FD6

// Redirect Lp::Utl::Limit::getSceneDbgMixCtrl_Pilot() to Lp::Utl::getCtrl()
@disabled
01A661A4 E0031FAA
01A661A8 1BFFFF17

// Force ChangeLanMatchInBoot to always be true
@enabled
01243B10 1F2003D5
01243B14 28008052

// Add newScene function for DbgSetting
@enabled
01C27E40 20F8E103
01C27E50 CC722200

// Add newScene function for FreeTest
@enabled
01C27E58 C8FAE103 
01C27E68 80702200

// Add newScene function for DummyMatch
@enabled
01C27E70 D0FAE103
01C27E80 20732200

// Lp::Sys::DbgTextWriter::pilotEntryV()
@enabled
-------- --------
-------- --------
-------- --------
-------- --------
-------- --------
-------- --------
-------- --------

やってみればわかるのですが、超めんどくさいコードになっています。

起動したらチュートリアルが始まるんだけど!?

っていう意見については仕様です。

// Cmn::Def::isDevelopSeq() always returns true
@enabled
0009CF44 20008052

これがオンになっていると、必ずチュートリアルから始まります。

オフにすればチュートリアルは始まらず、セーブを使うことができるのでめんどくさいかたはそっちでどうぞ。

チュートリアル後にショップに入るとDebugMenuが起動します。

何も表示されないDebugMenu

何も表示されてないんだけど!?

という意見についてもこれも仕様です。

// Lp::Sys::DbgTextWriter::pilotEntryV()
@enabled

というコードが空っぽなのが原因ですね。先程も言いましたが、Khang06氏のコードを3.1.0向けに移植すればテキストが表示されるようになります。

// Lp::Sys::DbgTextWriter::pilotEntryV() for 1.0.0 [Khang06]
@enabled
0120828C 82030014
0120748C 16000014
01207490 CF000014
012074B4 18010014
012074B8 55010014
012074E0 57020014
012074BC 0E020014

つまり、上のコードを3.1.0向けに移植すればいいということです。

つまり、この記事はこのコードを移植するだけでDebugMenuが使えるようになるというチュートリアルなわけです。

全部は無理でもこれだけならイケるっしょ。

自分で移植しても良かったのですが、今回はMeshi氏が既に移植していたのでそのコードを使わせてもらいました。

Meshi氏のコード適用後

ちゃんとテキストが表示されてる!

正直、ここをやるのはめっちゃめんどくさかったので先にしていただいていたのはすごく助かりました。

実際にやってみた

自分はいちいちShopをひらくのがめんどくさかったので、Boot時にDebugMenuが起動するようにしてあります。

// Cmn::Def::isDevelopSeq() always returns true
@enabled
0009CF44 20008052

// Don't initialize Cmn::SoundMic
@enabled
001C5BC8 1F2003D5

// DbgSetting in Cstm::newScene<TitleForShow::Scene>(sead::Heap*)
@enabled
-------- E0FF8152
-------- --------

// DummyMatch in Cstm::newScene<ThanksForShow::Scene>(sead::Heap*)
@enabled
00227350 E0FF8152
00227360 07714094

// Dummy out Lp::Sys::cpAssertProduct()
@enabled
019A7E14 C0035FD6

// Redirect Lp::Utl::Limit::getSceneDbgMixCtrl_Pilot() to Lp::Utl::getCtrl()
@enabled
01A661A4 E0031FAA
01A661A8 1BFFFF17

// Force ChangeLanMatchInBoot to always be true
@enabled
01243B10 1F2003D5
01243B14 28008052

// Add newScene function for DbgSetting
@enabled
01C27E40 20F8E103
01C27E50 CC722200

// Add newScene function for FreeTest
@enabled
01C27E58 C8FAE103 
01C27E68 80702200

// Add newScene function for DummyMatch
@enabled
01C27E70 D0FAE103
01C27E80 20732200

// Lp::Sys::DbgTextWriter::pilotEntryV()
@enabled
-------- --------
-------- --------
-------- --------
-------- --------
-------- --------
-------- --------
-------- --------
DebugMenu移植に成功

何か間違っていたら連絡ください。記事は以上。