[Hack] カンケツセンを解析する

EventGeyserの構造

Shadowninja氏の解析により、EventGeyserは以下のような構造を持つことが知られている。

Game::Coop::EventGeyser struc
actor            Cmn::Actor ?
stateMachine     Lp::Utl::StateMachine ?
sead::Random398  sead::Random ?
sead::Random3A8  sead::Random ?
qword3B8         sead::PtrArrayImpl ?
sakelineGeyser   DCQ ?
dword3D0         DCQ ?
qword3D8         DCQ ?
qword3F0         DCQ ?
float3F8         DCFS ?
float3FC         sead::Vector3 ?
Geme::Coop::EventGeyser ends

sead::Random398

Game::Coop::EventGeyser::EventGeyser(void)で定義される疑似乱数。

何に使われているのかは現在のところ不明。

sead::Random3A8

Game::Coop::EventGeyser::EventGeyser(void)で定義される疑似乱数。

カンケツセンのアタリの位置をシャッフルするために使われており、ここの値を変えるとアタリの位置が変わります。

qword3B8

sead::PtrArrayImplと呼ばれるポインタ配列でカンケツセンのアタリの位置が保存されています。

以下のコードはポインタ配列をシャッフルする`sead::PtrArrayImpl::Shuffle()を無効化し、アタリの位置を常に固定することができます。

// Disable sead::PtrArrayImpl::shuffle [tkgling]
@disabled
006DBFD4 1F2003D5

さて、ではなぜこのコードでアタリ位置が固定になるかを考えてみましょう。

ただ、アタリの位置を変えることができないのでステージによってはめんどくさい場所がアタリになります。

また、通常潮と満潮潮によって固定されるアタリの位置が違うこと・ルートまでは固定されないことが明らかになっています。

当初、sead::PtrArrayImplにはアタリのカンケツセンの順列が入っていると考えていました。つまり、アタリの位置がA→B→C→Dならsead::PtrArrayImpl=[A, B, C, D]という具合ですね。

が、どうもそれは誤りだったようでこの値はカンケツセンが閉じる毎に更新されるようなのです。

そして、sead::PtrArrayImplは以下のような構造をしています。

struct sead::PtrArrayImpl
{
  unsigned int count; // 0x3B8 #Geyser
  int64 *ptr;         // 0x3C0 *ptr
}

0x3B8にはカンケツセンの数が入っており、0x3C0には配列へのポインタが入っています。そして、このメモリ上の0x3C0に保存されているアドレスに配列の実体があるというわけなのです。

なぜこれがわかったのかということものちのち解説しています。

float3FC

カンケツセンのアタリの位置の座標が入っています。

ここを変更するとアタリのカンケツセンの表示される位置が変わります。

以下のコードはShadow氏によって副次的に与えられたコードで、適用するとカンケツセン開始時(残り1秒の段階)でアタリのカンケツセンが消えます。

// Disappear Succ Geyser [Shadow]
@disabled
006DC194 817380D21F6921F8

アタリのカンケツセンが消えるということは当然クリアできないのですが、何かしらの役に立てたい方はどうぞ。

正確にはアタリのカンケツセンの座標が(0, 0, 0)にとんでいるだけなので、ひょっとしたらハイプレならあけられるかもしれない。

sead::PtrArrayImplの中身を探る

このポインタ配列の中身が何になっているかはかなり謎だったのですが、EventGeyser::cleanupGeyser_()を読み解くことによって少し理解が進みました。

この関数はキンシャケがゴールのカンケツセンに吸い込まれたあとにアタリのカンケツセンとゴールのカンケツセンの吹き出しを止めたあとで、その他すべての吹き出しを止めてすべてのカンケツセンが開いていない状態に戻す関数です。

頑張ってアセンブラを読んでわかりやすいC++疑似コードに直したところ、以下のような感じになるみたいです。

u64 cleanupGeyser(u64 this)
{
  u32 count = this[952]; // #0x3B8
  u32 v1;
  u32 v2;

  for (u32 i = 0: ++i)
  {
    // Insert v2 and v3.
    count <= i ? v1 = 0 : v1 = this[960 + 8*i]; // v1
    count == 0 ? v2 = 0 : v2 = this[960]; // v2

    if (v1 == v2) // Compare with this[960 + 8*i] and this[960]
    {
      if (this[968] and (this[2680] | 1 ) == 11) // #0x3C8 and #0xA78
      {
       disappear_StartGeyser(v2); // v5 means Succ position?
      }
    }
    else if (v1 == this[984])
    {
      if (this[968])
      {
        if (this[2680] == 14 || this[2680] == 8)
        {
          disappear_GoalGeyser(v1);
        }
      }
    }
    disappear(v1);
  }
}

v1==v2のときだけdisappear_StartGeyser()が呼び出されるのですが、StartGeyserというのはつまりはアタリ位置のことなので、シャッフルした結果配列の先頭にきているカンケツセンがアタリになるということです。

そしてループごとにアドレスを+8していることからも、カンケツセンの配列の一つあたりのサイズが8*8=64bit(__int64)であることがわかるわけです。

アタリの位置を変えるには

さて、肝心のアタリの位置の変え方・求め方ですが、shuffle()を弄るのはよくありません。

なぜなら、この関数はカンケツセンのアタリを求めること以外にも利用されているのでここを変えると他の関数にも問題が発生するためです。

stateEnterIdle_

アセンブラ自体は以下のようになっており、乱数へのポインタとポインタ配列の先頭アドレスを渡してポインタ配列をシャッフルしていることがわかります。

006DBFC4  ADD  X20, X19, #0x3B8
006DBFC8  ADD  X21, X19, #0x3A8 ; sead::Random *
006DBFCC  MOV  X0, X20 ; this
006DBFD0  MOV  X1, X21
006DBFD4  BL   _ZN4sead12PtrArrayImpl7shuffleEPNS_6RandomE

シャッフルに使われる乱数を固定

ではここでsead::Randomに任意の値を代入してシャッフルする際に生成される乱数の値を変更することが可能かチャレンジしてみましょう。

こんな感じに書き換えれば乱数の代わりに好きな値を代入できます。

006DBFC4  ADD  X0, X19, #0x3B8
006DBFC8  ADD  X1, X19, #0x3A8 ; sead::Random *
006DBFCC  MOVZ	X2, #0x1C3F
006DBFD0  STR	X2, [X1]
006DBFD4  BL   _ZN4sead12PtrArrayImpl7shuffleEPNS_6RandomE

ただ、一命令しか書けないのであんまり大きい値は代入できないのだ。

// Change sead::Random
@disabled
006DBFC4 60E20E9161A20E91
006DBFCC E28783D2220000F9

これでどんなシードを使ってもアタリの順列が同じになるだろ!と思ったらポインタ配列自体がそもそもシードによって変わるらしく意味がなかったです。

位置をスワップする

006DBFC4  LDR	X20, [X19, #0x3C0]
006DBFC8  LDR	X21, [X20]
006DBFCC  LDR	X22, [X20, #0x18]
006DBFD0  STR	X21, [X20, #0x18]
006DBFD4  STR	X22, [X20]

配列の先頭が常にアタリで、シャッフルしていない場合は常にID==0のものが参照されているのでそれをID==1と置き換えればアタリの位置を変えられるはずです。

ただし、このコードはカンケツセンの初期化時に毎回呼び出されるのでその度に値が入れ替わるのでアタリの位置が1→0→1→0→1→0というように代わります。

それを実現するコードが以下のもので5.1.0で動作するので是非どうぞ。

// Swap Geyser Position [tkgling]
@enabled
006DBFC4 74E241F9950240F9960640F9
006DBFD0 950600F9960200F9
カンケツセンの位置スワップ

ただ、同期できないので非改造の端末と通信するとおかしな現象が起きるので注意しましょう。

位置固定化

一度だけスワップされるならいいのですが、何度も呼び出されてその度に入れ替えが起こるのでアタリの位置が交互になってしまいます。

ということは、配列に入っているカンケツセン情報をスワップではなく上書きしてしまえば何度呼び出されても必ず同じ値が選ばれるのでアタリの位置を固定化できるはずです。

// Fixed Geyser Position [tkgling]
@enabled
006DBFC4 74E241F9950640F9950200F9
006DBFD0 1F2003D51F2003D5
常にアタリの位置が固定

うーん、でも本来あるはずのカンケツセン情報を消してしまうといろいろ問題がありそうな気がする。

まあこの予想は正しくて、cleanup()Forでループさせてるだけだから該当するのがなくて挙動がおかしくなります。説明するのも大変なので、詳しくは動画で。

固定化に伴う反応しないカンケツセン

まあでも、ここまでわかったら別の方法でちゃんと対応できそうな気がします。

各ステージのカンケツセン位置

各ステージの内部IDをメモしておきます。

別に今更Wikiの命名規則を変えろとかそういうつもりはないので、内部データに興味がある方だけどうぞ。

シェケナダム

2がアタリなら必ずコンテナ前を通るのでここがアタリが一番美味しいかもしれません。

難破船ドン・ブラコ

ドンブラコのカンケツセンはバグってます。

いや、バグってないかもしれんけど他のステージとは違う扱いになってます。

多分、消された例のカンケツセンが原因です。残った二つのうちどちらかがid=2でどちらかがid=3のはずなんですが現時点ではわかりませんでした。

海上集落シャケト場

絶妙に気持ち悪い配置をしているシャケト場のカンケツセン。

0がアタリが最もカスで、続いて1と3がカス。2と4がアタリなら絶対部屋の中を通るので美味しい。

トキシラズいぶし工房

真っ直ぐで直線距離が近い3か、くねくねしていてピロピロしやすい4がオススメ。

0と1は論外だし、2は運びにくいのでナシ。

朽ちた箱舟ポラリス

1が大当たりなのは言うまでもないが、次にアタリなのは意外なことに0。

3は距離があるくせに途中にある1が邪魔をしてピロリしにくいし、運びやすい位置までくるのに実は時間がかかる。

2は単純に距離が遠いのでナシ、開けにくいし。

豆知識

今回の解析でわかったことをまとめておきます。

どこがアタリかは(おそらく)完全に均等。

完全解析できていないのでなんともですが、shuffle()が偏りのないように見えるのでどこがアタリやすいとかはないはず。

アタリ位置は(理論上)予測できる。

shuffle()自体は偏りのない関数ですが、sead::Randomで初期化されているのでここの値がわかれば完全に先読みできます。

もちろん、事前にそんなことはわからないので実際にはランダムのように振る舞います。

現状の方法よりもより良い開栓手順はない。

shuffle()に何かしらの欠点(偏り)がない限り、現状よりも良い開栓手順はありません。wikiの手順なりをしっかり覚えましょう。

そして、シャケト場に救いがないこともここからわかります。

ゴールの位置はアタリの位置と無関係。

今回の手法でアタリ位置を固定しても、ルートまでは固定できませんでした。つまり、別のところで決定されているようなのです。

最終的にはゴール位置まで固定できるようになりたいですね。

shuffle()が解析できれば最良のカンケツセンのシード値が求められる。

shuffle()自体はたった20行程度のコードなので、完全解析ができないはずはありません。

完全解析できれば予めすべての初期シードにおけるカンケツセンのアタリ位置とルートを求めることができます。

金イクラのドロップ位置はホスト管理。キンシャケの出現位置は各クライアント管理。

説明が長くなるのでこれについては詳細は割愛します。

まとめ

随分悩んでいてStarlightでも使ってコツコツ解析するかと考えていたところに突然アイデアの神が舞い降りてきて解決することができました。

いや、ヒントはたくさん転がっていたんですけれどそれを理解できなかった自分の脳みそがポンコツなだけなんです、はい。

次はshuffle()の解析と、アタリ位置の完全予測したいですね、楽しそうなので。

記事は以上。