Re:ゼロから始まるわけでもないハッシュ値生成
毎度おなじみふざけたタイトルから始まるデンパ時計のビーセイ記事でございます。
今回は曲のハッシュ値についてお話していきます。
そもそもハッシュ値なんてどこで使われてるの?って話なんですけども、BeatSaberでSongCoreというModを入れるとCustomSongs内にある曲がハッシュ値で区別されるようになるんですね。
バニラの状態であればハッシュ値ではなく曲のタイトルで管理されます。
なのでユーザーさんがこのハッシュ値を気にすることはほとんどありません。
曲のハッシュ値を確認してみよう
というわけでグダグダ説明を読むより見るのが一番!
ハッシュ値を見て見ましょう。
まずはインストールフォルダを開いてその中の「Playlists」フォルダを探しましょう。
その中には.jsonやら.bplistやら入っているはずですが適当にメモ帳か何かで開きましょう。
僕はプログラマなのでVisual Studio Codeを使います(ドヤァ
開くとこんな感じになります。
これはゲーム中に利用できるプレイリストのデータたちですね。それぞれに役割があるのですが今回は「songs」に注目しましょう。
ここにはそのプレイリストに含まれる。曲の情報が詰まっています。
よく見ると「hash」と書かれた行が見えますね。
そうです。ここに書かれている英数字こそその曲に割り当てられたハッシュ値なのです。
このハッシュ値はプレイリスト以外ではScoreSaberのサイトにIDとして表記されています。
曲のハッシュ値の作られ方
さて、このハッシュ値。一体どのように決められているのでしょうか?
キーワードはSHA1!(シャーワン)
グーグル先生で検索すると暗号がどうたらとかなんかいろいろ書いてますが、超簡単に言うと「どんなデータでも40文字にしちゃう仕組み」とだけ覚えてればこの記事は理解できます。
とはいえ、この後の説明で心がバッキバキに折られないようデータとはなんぞや。のお話を少しだけ。
パソコンで扱うデータは0と1だけで表すことができます。
この0と1の並び方でそのデータが画像なのか音楽なのか動画なのかが決まります。
もちろんBeatSaberの譜面のデータも例外ではありません。
さて、この0と1の長さ。一体どれくらいなのでしょうか?
よくデータの大きさを表すのに○○バイトという言葉を耳にしたことはありませんか?
このバイトというのは単位の一種で、8ビットが1バイトになります。
いやいや、1ビットってなんやねん
急に出てきたこの1ビット。
これが0と1を表す最小の単位です。
0011と並んでいたら4ビット。
0011 1101と並んでいたら8ビット。
0011 1101 1111 1010と並んでいたら16ビット。
という感じですね。
さすがにビットだと数が大きくなりすぎてしまうのでバイト単位で数えるわけです。
鉛筆12本を1ダースで数えるとかと同じ感覚で大丈夫です。
さてさて、話は戻りまして曲のハッシュ値の生成方法について説明していきましょう。
```
public static string GetCustomLevelHash(CustomBeatmapLevel level)
{
if (GetCachedSongData(level.customLevelPath, out var directoryHash, out var songHash))
{
return songHash;
}
byte[] combinedBytes = new byte[0];
combinedBytes = combinedBytes.Concat(File.ReadAllBytes(level.customLevelPath + '/' + "info.dat")).ToArray();
for (var i = 0; i < level.standardLevelInfoSaveData.difficultyBeatmapSets.Length; i++)
{
for (var i2 = 0; i2 < level.standardLevelInfoSaveData.difficultyBeatmapSets[i].difficultyBeatmaps.Length; i2++)
{
if (File.Exists(level.customLevelPath + '/' + level.standardLevelInfoSaveData.difficultyBeatmapSets[i].difficultyBeatmaps[i2].beatmapFilename))
{
combinedBytes = combinedBytes
.Concat(File.ReadAllBytes(level.customLevelPath + '/' + level.standardLevelInfoSaveData.difficultyBeatmapSets[i].difficultyBeatmaps[i2].beatmapFilename)).ToArray();
}
}
}
string hash = CreateSha1FromBytes(combinedBytes.ToArray());
cachedSongHashData[level.customLevelPath] = new SongHashData(directoryHash, hash);
return hash;
}
public static string CreateSha1FromBytes(byte[] input)
{
// Use input string to calculate MD5 hash
using (var sha1 = SHA1.Create())
{
var inputBytes = input;
var hashBytes = sha1.ComputeHash(inputBytes);
return BitConverter.ToString(hashBytes).Replace("-", string.Empty);
}
}
こちらがSongCoreで実際に使われているハッシュ値生成のコードです。
わかるか
ですよねー!
僕もこれ一個一個解説していくのはめんどくさいので何やってるかを箇条書きで書いていきます。
・曲のフォルダの中からinfo.datファイルを探す。
・info.datの中に書かれている難易度の.datを探す。
・見つかったファイルをそれぞれバイト配列にして、それらを全部くっつけて一つのバイト配列にする。
・できたバイト配列をSHA1を使ってハッシュ値にする
こんな感じです。
ここで重要なのはバイト配列の対象となるファイル達です。
例えば下の画像のような構成なら
・info.dat
・EasyStandard.dat
・NormalStandard.dat
・HardStandard.dat
・ExpertStandard.dat
・ExpertPlusStandard.dat
になります。
カバー画像とか音楽ファイルはハッシュ値には影響しません。
というわけで簡単にハッシュ値が分かるアプリを作りました。
フォルダを2個指定するとそれぞれのハッシュ値を返してくれる簡単なアプリです。
今回はこのエライエライエライ!を使ってハッシュ値生成の仕組みを見ていきましょう。
まず、なにもやっていないWIPのハッシュ値は660D404D2B53173001FAC9899CEA168BFB51502Fです。
次に、サーバーから落とした公開されたエライエライエライ!のハッシュを見ていきましょう。
上がWIP(660D404D2B53173001FAC9899CEA168BFB51502F)、下が公開されたもの(54D6D2E1280D287D1501822C98BB0F996C8DFCFC)。何やらハッシュ値が違いますね。
この違いはinfo.datに隠されています。
左が落としてきた譜面のinfo.datで、右がWIPに入っているinfo.datです。
違いとして、整形のためのスペースや改行が追加されていること。
「_songFilename」の項目がsong.oggからsong.eggになっていること。
あたりですね。
この違いによりinfo.datのバイト配列が変わるため、サーバーに上げた譜面と自分が持っているWIP譜面のハッシュ値が変わってしまうわけですね。
他のファイルがハッシュ値に影響を与えないことを確認するため、カバー画像と曲ファイルを消してみましょう。
上が何もしていないWIP、下が上のフォルダを丸ごとコピーしたのち、曲ファイルとカバー画像ファイルを削除したもの。
同じハッシュ値が生成されていますね。
これにより.dat以外がハッシュ値に影響を与えないことが確認できました。
次にinfo.datの最後の行に改行を入れてみましょう。
上が何もしていないWIP、下がinfo.datの最後に改行1つ加えたもの。
もはや、まったく別のハッシュ値が生成されていますね。
というわけで、対象となるファイルのバイト配列が少しでも変わると全く違うハッシュ値が生成されるということが分かります。
まとめ
ハッシュ値は曲情報から生成される。
落としてきた譜面は絶対いじらないこと。いじるとハッシュ値が変わってしまう。