Minecraft 1.20.x forgeでMOD開発 時間で発火するイベント

こちらの続きです。

https://qiita.com/simoyama2323/items/295ee1c04add7d3b3e97

## 今回やりたいこと

今回はゲーム内の時間を検知して発火させるイベントを作っていきます。
具体的には朝と夜にサーバー内の全員にシステムメッセージを送信します。

## 前提知識
Minecraftには2種類の時間があるみたいです。
- gametime : ゲームサーバーが起動してからの時間
- daytime : ゲーム内の1日の時間

ゲーム内で1日というのは0~24000Tickという表現がされています。**(後述しますが24000を超える場合有)**
1Tickは現実の時間で約1/20秒（0.05秒）です。つまり、1秒間に20Tickが進行します。

詳しくはwikiで

https://minecraft.fandom.com/ja/wiki/%E6%98%BC%E5%A4%9C%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB

https://minecraft.fandom.com/ja/wiki/%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89/time

以降この記事ではdaytimeが0~12999を朝 13000~24000を夜と表現します。

## コード

### 時間のイベントハンドラ
```java
    @SubscribeEvent
    public void onTickStarting(TickEvent.ServerTickEvent event)
```

サーバー側でTickを検知するには```TickEvent.ServerTickEvent```を使います。
***注意点があり、このイベントは毎Tickの始まりと終わりに発火します。***
要はこのままだとリアルタイムの1分間に1200回この処理が発火することになります。
なので以下のようにTickの始まりか終わりかだけを判定する条件を書いておきます。（それでも多い気がしますが、、、）

```java
    @SubscribeEvent
    public void onTickStarting(TickEvent.ServerTickEvent event) {
        // 毎Tickの終わりにだけ処理 かつ サーバー側でのみ処理
        if (event.phase == TickEvent.Phase.END && event.side == LogicalSide.SERVER) {
            // 処理
        }
    }
```

### 朝と夜の判定

```java
    // 処理を一回しかさせないフラグ
    private boolean night = false;

    @SubscribeEvent
    public void onMorningStarting(TickEvent.ServerTickEvent event) {
        if (event.phase == TickEvent.Phase.END && event.side == LogicalSide.SERVER) {
            MinecraftServer server = event.getServer();
            long currentDaytime = server.getWorldData().overworldData().getDayTime() % 24000;

            // 朝
            if (!night && currentDaytime >= 0 && currentDaytime < 13000) {
                LOGGER.debug("Server DayTime: {}", currentDaytime);
            }
        }
    }

    @SubscribeEvent
    public void onNightStarting(TickEvent.ServerTickEvent event) {
        if (event.phase == TickEvent.Phase.END && event.side == LogicalSide.SERVER) {
            MinecraftServer server = event.getServer();
            long currentDaytime = server.getWorldData().overworldData().getDayTime() % 24000;

            // 夜
            if (night && currentDaytime >= 13000 && currentDaytime < 24000) {
                LOGGER.debug("Server DayTime: {}", currentDaytime);
            }
        }
    }    
```

普段から負荷を考えている人が見ると発狂しそうなコードですが、このような形が一般的だそうです。
一つのイベントで時間の判定を2回入れるのと、イベントを分けて2つ処理するのどっちがいいのか迷ったんですが、可読性を考えてイベント単位で分けました。
このイベント自体毎Tick呼ばれるおかげでこの範囲に収まっているときは毎回動いてしまうので、night変数でフラグ管理をして一回だけ動くようにしています。

#### 24000の剰余を求めている理由
最初はこの処理を入れていなかったのですが、ちょいちょい動かない時があって```server.getWorldData().overworldData().getDayTime()```を毎秒見てみたら24000を超えることがありました。
```0 == 24000``` ではないのはなんとなく理解はできるのですが、よくわかりませんでした。
このゲーム自体だいぶ長いのでバグではなく仕様だと思うのですが、結構重要なのでどこかに書いてほしい。。。
こんな理由から余剰を求めて24000を超えた時にも対応しています。

### ログインしているプレイヤー全員にシステムメッセージを送信

```java
    @SubscribeEvent
    public void onMorningStarting(TickEvent.ServerTickEvent event) {
        if (event.phase == TickEvent.Phase.END && event.side == LogicalSide.SERVER) {
            MinecraftServer server = event.getServer();
            List<ServerPlayer> serverPlayers = server.getPlayerList().getPlayers();
            long currentDaytime = server.getWorldData().overworldData().getDayTime() % 24000;

            // 朝
            if (currentDaytime >= 0 && currentDaytime < 13000) {
                for (Player player : serverPlayers) {
                    player.sendSystemMessage(Component.nullToEmpty("朝です"));
                }
            }
        }
    }

    @SubscribeEvent
    public void onNightStarting(TickEvent.ServerTickEvent event) {
        if (event.phase == TickEvent.Phase.END && event.side == LogicalSide.SERVER) {
            MinecraftServer server = event.getServer();
            List<ServerPlayer> serverPlayers = server.getPlayerList().getPlayers();
            long currentDaytime = server.getWorldData().overworldData().getDayTime() % 24000;

            // 夜
            if (currentDaytime >= 13000 && currentDaytime < 24000) {
                for (Player player : serverPlayers) {
                    player.sendSystemMessage(Component.nullToEmpty("夜です"));
                }
            }
        }
    }    
```

あとは消化試合で、イベントからサーバー内のプレイヤーを取得してメッセージを送ります。
以上です。

## その他
### forgeのバージョンとMinecraftのバージョンは必ずマイナーバージョンまで合わせましょう
これを検証しているとき、ちょうどMinecraftが1.20から1.20.1にアップデートされていました。
それに気づかず1.20用のFrogeを使っていたせいで何してもこの```TickEvent```以下のイベントが発火しませんでした。
必ず合わせましょう。

### TickEvent.WorldTickEventは1.20以降では使えない
時間で発火するイベントの実装方法を調べていると以下のようにWorldTickEventを使う旨のFormが出てきます。（というかこれしか出てこない）

https://forums.minecraftforge.net/topic/28451-worldtickevent-running-multiple-times/

正確には1.19からかもしれませんが、メソッド自体無くなっているのであきらめましょう。
消した理由としてはClientTickEventとServerTickEventで対応できるからでしょうか？

## 参考
記事内に記載済み


次回はカスタムコマンドを実装していよいよ完成。