バニラでアップデート通知システムを作る

この記事は [Minecraft Command Advent Calendar 2023](https://qiita.com/advent-calendar/2023/mccmd) 25 日目の記事です。

## はじめに
データパックや配布マップをアップデートした際にプレイヤーにそれを通知したいと思ったことはありませんか？
コマンドをある程度わかっている方であれば、その手のシステムをバニラで実装するのは不可能だと諦めていたかもしれません。
ですが、最近のアップデートによる技術革新等によりバニラでアップデート通知システムを作ることに成功したので、この記事ではどのようなアプローチでそれを実装したかを解説していきます。

この記事では、以下に関する知識を仮定します。
- コマンド ･ データパック ･ マクロの基礎
- storage の概念
- NBTPath による NBT 操作

また、この記事のコードは全て 1.20.4 で動作確認を行っています。
最後に、完全なコードは [GitHub](https://github.com/ChenCMD/MCCMD-UpdateNotifier) に置いてあります。
煮るなり焼くなり好きにしてください。ただし自己責任でお願いします。

## そもそもなぜ出来ないと思われていたか
一言で言えば ｢アップデートの有無を取得する方法がない (と思われていた) から｣ です。
Minecraft には多くのコマンドがありますが、アップデート有無を取得できる外部サーバーへのリクエストを行えるようなコマンドはありません。
また、Minecraft クライアントからの通信はマルチサーバーへの通信を除いてほぼ全てが Mojang 側のサーバーへの通信となっていて、そこにおいても任意の外部サーバーへのリクエストは不可能でした。[^1]

[^1]: 細かいことを言えば、マルチサーバーへの通信以外にもサーバーリソースパックの DL 元サーバー等も Mojang 以外への通信ではありますが、まぁどうせ使えないのでそんなことはどうでもよいです。

## 今回取ったアプローチ
まず、アップデート有無を取得できるためにはどのような条件が必要なのかを考えると、
- ゲーム内部から取得できる値である
- 開発者が任意のタイミングでゲーム外部から変更できる値である
ざっとこの二点が挙がります。

こんな条件を満たすものが存在するのかと思われるかもしれませんが、実は偶然にも一つだけ存在します。
それは ｢プレイヤーのスキン｣ です。
プレイヤーのスキンはご存知の通りゲーム外部から変更することができ、ゲーム内部では player_head から取得することが出来ます。

## player_head の仕様
player_head は以下のようなコマンドで設置することでプレイヤーのスキンが適用されます。
```mcfunction
setblock 2023 -64 1225 minecraft:player_head{SkullOwner:{Name:"<プレイヤーの ID>"}}
```
ただし、スキンサーバーからのプレイヤーのスキンの取得は非同期で行われるため設置後 10 tick 程度待つ必要があります。[^2]
また、取得したスキンの情報はサーバーにキャッシュされ、サーバーを再起動するまでは再度設置した際にキャッシュを参照します。

[^2]: この仕様に気づかずデータが reload 時しか取得できない問題に遭遇し、2 時間程度頭を抱えてました。つらかった。

設置された player_head には以下のような構造でデータが保存されています。
```yml
{
  SkullOwner: {
    Name: <プレイヤーの ID>,
    Properties: {
      textures: [
        {
          Signature: <テクスチャのシグネチャ>,
          Value: Base64 エンコードされたスキンの JSON データ .... 中身: {
            timestamp: <スキンの取得時刻？>,
            profileId: <プレイヤーの UUID (ハイフン無し)>,
            profileName: <プレイヤーの ID>,
            signatureRequired: <true なんこれ>,
            textures: {
              SKIN: {
                url: <スキンサーバー上のプレイヤーのスキンの URL>,
                metadata: {
                  model: <スキンのモデル classic or slim>
                }
              }
            }
          }
        }
      ]
    }
  }
}
```

このデータの中で開発者側がスキンを変更することで制御できる値は、`SkullOwner.Properties.textures[0].Value` の Base64 文字列をパースした結果の `textures.SKIN.url` です。
この値は Minecraft ランチャーでスキンを登録した際に、スキンの画像に対して発行される URL です。
この URL は登録した時点で発行され、削除しない限りは別のスキンを利用しても元のスキンに戻したとしても同じ URL を扱います。[^3]

[^3]: このスキンの URL がスキン画像に対して冪等なものなのか、登録時点でランダムに発行されるものなのかは検証できてないです。多分後者だとは思うけど。

## 実装
`SkullOwner.Properties.textures[0].Value` をパースして `textures.SKIN.url` を見ればスキンの変更を検知できることがわかったので、ひとまずは ｢リリース時点のスキン URL と異なるスキン URL が取得できた場合はアップデートがリリースされた｣ として扱う形で実装を行います。

### Base64 パーサー
最初に `SkullOwner.Properties.textures[0].Value` の中身読むために Base64 パーサーを実装します。
ですが、Base64 パーサーの実装について*ここに記すには余白は狭すぎる*ので、20 日目の記事で解説を別途行っています。
詳しくはそちらを参照してください。
https://qiita.com/ChenCMD/items/d7a7f9cabdcc7479ccdd

### アップデートチェックシステム
次に呼び出す部分などは置いておいて、このシステムの本質であるアップデートチェックシステムを実装してみます。[^4]
また、この時点では setblock 後に遅延させるような処理を作っていないため、
```mcfunction
setblock 2023 -64 1225 minecraft:player_head{SkullOwner:{Name:"<プレイヤーの ID>"}}
```
で設置しスキンがロードされた後に手動で `update_notifier:check_update/_` を実行する必要があります。

[^4]: 全てを実装した後に切り出しているので、もしかしたらここのコードは動かないかも。動かなかったらごめんね。

#### update_notifier/functions/check_update/_.mcfunction
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/98dea085-6452-650e-1051-ff3a63a9124b.png)

#### update_notifier/functions/check_update/parse_value.m.mcfunction
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/21ee8c6f-5e3f-fb05-a59c-f8deaa89dcdf.png)

### データパックとしての体裁を整える
データパックの実装としては、
1. サーバーのロード時にアップデートを確認
2. アップデートが存在する場合は JOIN したプレイヤーにアップデート通知メッセージを出力する
3. メッセージのスキップをクリックした場合は、次の更新までメッセージを出力しなくする
といった簡易的かつ最低限必要な実装とします。

各関数の細かい解説は申し訳ないのですが力尽きたので省きます。ある程度の説明はコメントに書いたのでそちらを確認してください。

#### [update_notifier/functions/load.mcfunction](https://github.com/ChenCMD/MCCMD-UpdateNotifier/tree/main/data/update_notifier/functions/load.mcfunction)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/d8aa456e-d556-c9db-f547-a73e845dd9d9.png)

#### [update_notifier/functions/initialize.mcfunction](https://github.com/ChenCMD/MCCMD-UpdateNotifier/tree/main/data/update_notifier/functions/initialize.mcfunction)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/79e214da-43be-93f6-2040-899fbad0d140.png)

#### [update_notifier/functions/tick.mcfunction](https://github.com/ChenCMD/MCCMD-UpdateNotifier/tree/main/data/update_notifier/functions/tick.mcfunction)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/ce8f79cf-5d0f-eee7-2adc-326946c47b30.png)

#### [update_notifier/functions/check_update/_.mcfunction](https://github.com/ChenCMD/MCCMD-UpdateNotifier/tree/main/data/update_notifier/functions/check_update/_)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/adbaa86e-7290-f5f3-df28-395d5dfdf00d.png)

#### [update_notifier/functions/player_joined.mcfunction](https://github.com/ChenCMD/MCCMD-UpdateNotifier/tree/main/data/update_notifier/functions/player_joined.mcfunction)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/39de149b-c2ed-012f-d59c-efda247f967a.png)

#### [update_notifier/functions/update_notification.mcfunction](https://github.com/ChenCMD/MCCMD-UpdateNotifier/tree/main/data/update_notifier/functions/update_notification.mcfunction)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988406/a9a5133d-efbb-fb85-c799-c55917b264b7.png)

## 動作確認
以下の順で動作確認を行ったデモ映像がこちらです。
1. スキンを変更せずにサーバーに join
2. サーバーを stop
3. スキンを変更
4. サーバーを開く
5. サーバーに join

https://x.com/CMD__Cat/status/1739574569248464920


かなりいい感じですね。

## 未解決の課題
### 1 アカウントで 1 配布物のアップデートしか扱えない
未検証ではありますが、スキンの登録時にスキン画像の URL が発行されることを利用して事前に大量のスキンを登録して置くことで理論上は 5 ~ 9 bit 程のデータを扱えるようになるので、複数のアップデートの有無をそこに持たせることが出来るかもしれません。[^5]

[^5]: ｢サーバー起動時に開発者が指定した数 bit の情報を流し込める｣ と考えると、アップデート通知以外にも利用用途があるかもしれません。面白そうな利用用途を思いついたら是非教えてください。

### スキンの変更が軽率に出来なくなる
こればっかりはどうしようもない気がします。アップデート管理用のアカウントを買うか前述の複数のスキン情報の登録を利用してごまかすしか無い気がします。

## まとめ
この記事ではプレイヤーのスキン周りの仕様を利用して配布物のアップデート通知システムを実装してみました。
実用に耐えるかは正直未知数ではあるものの、｢使えるものは何でも使う｣ 主義のコマンド勢にとってはなかなか夢のあるシステムなのかなと個人的には思います。
ちょっと[^6]遅れてしまいましたが、この技術をプレゼントとしてクリスマス当日の記事をここで終わりとします。

[^6]: 記事を書き終えた時点で 12/25 の 30 時ですが、誤差です。

## 参考にさせていただいたサイト
- [Base64 - Wikipedia](https://ja.wikipedia.org/wiki/Base64)
- [ASCII - Wikipedia](https://ja.wikipedia.org/wiki/ASCII)
- [Head  Minecraft Wiki](https://minecraft.fandom.com/wiki/Head#Block_data)