[Starlight] SeedHackの概要と解説

SeedHackとは

適当に考えた造語なので特に意味はない。

まあ簡単にいえばシードの値を制御することでサーモンランにおけるランダムな振る舞いを予測可能にしてしまうHackだと思っていただきたい。

まあSeedHackについて解説する前に、まずはゲームにおける乱数の重要性とその仕組みについて解説したのでそれを読んでいただこう。

乱数の実装ミス

スプラトゥーンに限らず、擬似乱数生成器は初期条件から乱数を生成するのだが何もせずに乱数を生成するとプログラムを呼び出すたびに同じ値を返してしまう。

関数を呼び出すたびではないことに注意していただきたい。

プログラムを呼び出すたびに~というのは一旦プログラムが終了して再度実行された時を意味する。

例えば、FF5では電源をつけたときに乱数生成器が起動するのだが初期値が固定されていたために本来なら滅多にエンカウントしないレアモンスターであるにもかかわらず、リセットしてタイトル画面からロードすると二度目の戦闘で必ずエンカウントするなどの実装ミスがあった。

このように乱数の実装が不適切だとその後のエンカウントテーブルなどが予測可能になってしまい、ユーザが大きく得をしてしまうのである。

そのため、乱数は初期化する際にシードと呼ばれる値で初期条件を予想できないようにする必要がある。

同じシードで初期化するとやはり乱数が予想されてしまうので、シードは実行されるたびに異なる値が良い。

よく使われるのがUNIX時間だったりするのだが、極めて高速にプログラムを実行するとシードとして同じUNIX時間が使われてしまう。

また、ユーザがコンピュータの時間を変更することで意図的にシードを変更することもできる。

このように一見簡単に見えるランダムな数の生成ですらプログラマは結構気を配らなければいけないのだ。

スプラトゥーンにおける乱数

実は疑似乱数生成アルゴリズムは完全に解明されている。

以下に載せておくので見ておくと良いことがあるかもしれません。

SeedHackが必要なわけ

結論から言ってしまえば、非改造機ともエラーなしにコードを実行するためです。

さて、スプラトゥーン(ここではサーモンランに限定します)におけるランダム性は全て生成されたシードから乱数をつくり、その乱数に従って生み出されています。

例えばシードとして1を使った場合、3, 2, 9, 7, 5というように乱数が生成されたとします(ここではわかりやすく生成される最大の整数を10としています)

これがサーモンランだと「3だとカタパッドがでる、2だとタワーがでる」 といったように制御されているわけです。なので、実際にゲームに影響を与えるのはシードではなくシードから生成される乱数なのです。

これは改造してあるスイッチであれば「生成される乱数として全て1を返してモグラしかでないようにする」といったコードを実装するのはそれほど難しくありません。

つまり、わざわざシードの値が何であるかは基本的には気にする必要がないはずなのです。

では、どうしてわざわざ苦労してSeedHackをするのでしょうか?

それは、乱数自体の値を上書きしてしまうHackは非改造機と通信するとDesyncを起こしてしまうためです。

コンテナ(@container12345)氏が発見したWAVE変更コードは、常にWAVEを固定できる極めて便利なコードでしたが、非改造機と通信するとDesyncを起こしてまともにプレイできないという問題を抱えていました。

断っておきますと、コードを適応すればDesyncを起こすのが普通なのでコンテナ氏のコードが悪いとかそういう意味ではありません。

そして、なんとか非改造機とも問題なくゲームができるようなWAVE操作ができないかと考えた末に発見されたのがSeedHackでした。

大雑把な仕組み

さて、みなさんがゲームをしているときに内部でどのようなことが行われているかわかりやすく図にしてみました。

基本的にはゲーム開始直後にシードが生成され、そこから乱数が発生しゲームのランダムイベントは制御されています。

これがコードが実行可能なデバイスですと、設定された乱数などをコードで上書きすることでゲームの内容を書き換えることができます。

サーモンランですと霧イベントでキンシャケが落とす金イクラの数は乱数で決定しているのですが、それを無理やり十個に書き換えることも可能なわけです。

先程のコードよりは難易度は上がりますが、値をそもそも乱数生成器につくらせないようなコードも実装できます。

どちらも得られる結果は同じですが、これらの方法によって常に夜イベントを発生させたりすることができるわけです。

何故この方法でダメなのか

実は一人で実行している分には全く問題ありません。

何故なら、本来のシードからはイベント0が発生するような乱数であったとしてそれを上書きしてしまってもゲーム内に矛盾が発生しないからです。

ほとんどすべてのゲームは乱数を使用する際に生成された乱数とシードの整合性チェックを行いません。

何故なら、乱数が途中で書き換えられることなど想定していないからです。

コードを利用する二つのデバイス間で通信する場合も問題ありません。

何故なら、どちらのデバイスでも最終的にコードによって乱数が上書きされるためです。

どちらもイベント1が発生するので矛盾は発生しないので違和感なくプレイすることができます。

ところが、コードを実行していないデバイスと通信すると矛盾が生じます。

ホストが送信している情報は乱数自体ではなくシードだからです。

「何故逐一データを送らないのか」と気になる方もいるかも知れません。

しかし、仮に逐次乱数を送信していると送受信に失敗することイコールイベントの同期失敗を意味し、致命的なエラーの原因となりかねません。

また、ゲーム内では頻繁に乱数を生成するので生成のたびにクライアントとホストの間で通信をするのはコスト的にも都合が悪いです。

ここで、先程の疑似乱数の性質を思い出してください。

FF5ではリセット時に “毎回同じシードで初期化される” ので二度目のエンカウントで必ずレアモンスターがでてくる、ということを。

そう、つまりシードさえ決めてしまえば同じ乱数生成器を使っている限りN番目に生成される乱数は同じなのです。

よって、いちいち乱数を通信でやり取りする必要はなく、最初の一回だけシードを送信すればいいことになります。

SeedHackというのは、この送信しているシードを書き換えてしまおうというものです。

SeedHackの流れ

SeedHackで大事なのは乱数生成器がつくった乱数は弄らないということです。

もしもここを弄るとホストとクライアントの間でDesync(非同期)が発生し、干潮グリルとか満潮ドスコイなどが起きてしまいます。最悪の場合、ゲームが強制終了することもあるでしょう。

SeedHackで常にイベント3を発生させたい場合、まずはイベントIDとして3を返すような上手いシードを探します。

乱数として3を返すようなシードでなくて良いことに注意しましょう。

乱数は最低でも32ビット(4億くらいだと思っていただければ良い)通りの値を返すので、直接3という乱数を返すシードを見つけるのは困難です。

例えば、イベントの種類が全部で5種類だとしたら “生成された乱数を5で割ったときのあまり” などでイベントIDを決定しています。

つまり、約20%の確率でイベントIDとして3を返すシードが見つかるというわけです。

このシードを見つけるのは面倒ですが、乱数から生成されるイベントIDの種類が10種類程度であれば総当たりでやってやれないことはありません。

ちなみに、上記の擬似乱数生成コードを完全に理解すれば好きなイベントIDを生み出すシードを逆算できます。

もちろんこれは総当たりで目的のシードを見つけるよりも遥かに難しいです。

上手いシードを見つけたら、シードの値をその値に変更するようなコードを書くだけです。

これを実装すれば非改造機との通信でも矛盾なくプレイ可能なはずです。

現在までに発見されたシード

W1に夜イベントが発生するシードは総当たり法により現在以下のものが判明しています。

満潮通常干潮潮未確認
ラッシュ2320
65535
None104
112
カンケツセン21714142None
ハコビヤ28
1000118
2006
7532
ドスコイNoneNone106
100
2000
10003
グリル913125None

container12345により完全解析されました

シードが取りうる約43億通りについてどのイベントが発生するかの完全解析が完了しました。

まとめ

SeedHackは非改造機と夜イベント共有ができるかなり画期的なHack。

その代わり、夜イベントを発生させるシードを見つけなければならず手軽さではコンテナ氏の強制イベント変更コードが勝る。

SeedHackを利用すれば誰とでもイカッチャで好きなWAVEが練習でき、しかも全く同じWAVEのために上達度もわかりやすくて便利なのではないか。

更に、特定の状況における理想的な動きの追求ができるのも大きい。

イカッチャではリトライし続ける限りは同じWAVEが体験できるが、一度でも通信が切れたり他のブキを使おうとするとまたWAVEが変わってしまうのがネックだった。

本手法はそれらを解決する画期的な手法になるのではないかと期待している、以上。