diff --git a/common/src/main/java/dev/architectury/event/events/common/ChunkWatchEvent.java b/common/src/main/java/dev/architectury/event/events/common/ChunkWatchEvent.java
new file mode 100644
index 00000000..124f7e51
--- /dev/null
+++ b/common/src/main/java/dev/architectury/event/events/common/ChunkWatchEvent.java
@@ -0,0 +1,62 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021, 2022 architectury
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package dev.architectury.event.events.common;
+
+import dev.architectury.event.Event;
+import dev.architectury.event.EventFactory;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.LevelChunk;
+
+public interface ChunkWatchEvent {
+ /**
+ * This event is fired whenever a {@link ServerPlayer} begins watching a chunk and the chunk is queued up for
+ * sending to the client.
+ *
+ * This event must NOT be used to send additional chunk-related data to the client as the client will not be aware
+ * of the chunk yet when this event fires. {@link ChunkWatchEvent#SENT} should be used for this purpose instead
+ */
+ Event WATCH = EventFactory.createLoop();
+
+ /**
+ * This event is fired whenever a chunk being watched by a {@link ServerPlayer} is transmitted to their client.
+ *
+ * This event may be used to send additional chunk-related data to the client.
+ */
+ Event SENT = EventFactory.createLoop();
+
+ /**
+ * This event is fired whenever a {@link ServerPlayer} stops watching a chunk. The chunk this event fires for
+ * may never have actually been known to the client if the chunk goes out of range before being sent due to
+ * slow pacing of chunk sync on slow connections or to slow clients.
+ */
+ Event UNWATCH = EventFactory.createLoop();
+
+ @FunctionalInterface
+ interface ChunkListener {
+ void listen(LevelChunk chunk, ServerLevel level, ServerPlayer player);
+ }
+
+ @FunctionalInterface
+ interface ChunkPosListener {
+ void listen(ChunkPos chunkPos, ServerLevel level, ServerPlayer player);
+ }
+}
diff --git a/fabric/src/main/java/dev/architectury/mixin/fabric/MixinChunkMap.java b/fabric/src/main/java/dev/architectury/mixin/fabric/MixinChunkMap.java
index 6c17c911..d3f6e82c 100644
--- a/fabric/src/main/java/dev/architectury/mixin/fabric/MixinChunkMap.java
+++ b/fabric/src/main/java/dev/architectury/mixin/fabric/MixinChunkMap.java
@@ -20,17 +20,21 @@
package dev.architectury.mixin.fabric;
import dev.architectury.event.events.common.ChunkEvent;
+import dev.architectury.event.events.common.ChunkWatchEvent;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkStatus;
+import net.minecraft.world.level.chunk.LevelChunk;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
@@ -48,4 +52,14 @@ public class MixinChunkMap {
private void save(ChunkAccess chunkAccess, CallbackInfoReturnable cir, ChunkPos pos, ChunkStatus chunkStatus, CompoundTag nbt) {
ChunkEvent.SAVE_DATA.invoker().save(chunkAccess, this.level, nbt);
}
+
+ @Inject(method = "markChunkPendingToSend(Lnet/minecraft/server/level/ServerPlayer;Lnet/minecraft/world/level/chunk/LevelChunk;)V", at = @At("TAIL"))
+ private static void watch(ServerPlayer player, LevelChunk chunk, CallbackInfo ci) {
+ ChunkWatchEvent.WATCH.invoker().listen(chunk, player.serverLevel(), player);
+ }
+
+ @Inject(method = "dropChunk(Lnet/minecraft/server/level/ServerPlayer;Lnet/minecraft/world/level/ChunkPos;)V", at = @At("HEAD"))
+ private static void unwatch(ServerPlayer player, ChunkPos chunkPos, CallbackInfo ci) {
+ ChunkWatchEvent.UNWATCH.invoker().listen(chunkPos, player.serverLevel(), player);
+ }
}
diff --git a/fabric/src/main/java/dev/architectury/mixin/fabric/MixinPlayerChunkSender.java b/fabric/src/main/java/dev/architectury/mixin/fabric/MixinPlayerChunkSender.java
new file mode 100644
index 00000000..cce2fae4
--- /dev/null
+++ b/fabric/src/main/java/dev/architectury/mixin/fabric/MixinPlayerChunkSender.java
@@ -0,0 +1,38 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021, 2022 architectury
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package dev.architectury.mixin.fabric;
+
+import dev.architectury.event.events.common.ChunkWatchEvent;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.network.PlayerChunkSender;
+import net.minecraft.server.network.ServerGamePacketListenerImpl;
+import net.minecraft.world.level.chunk.LevelChunk;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(PlayerChunkSender.class)
+public class MixinPlayerChunkSender {
+ @Inject(method = "sendChunk", at = @At("TAIL"))
+ private static void send(ServerGamePacketListenerImpl packetListener, ServerLevel level, LevelChunk chunk, CallbackInfo ci) {
+ ChunkWatchEvent.SENT.invoker().listen(chunk, level, packetListener.player);
+ }
+}
diff --git a/fabric/src/main/resources/architectury.mixins.json b/fabric/src/main/resources/architectury.mixins.json
index f8688e50..e2a96df9 100644
--- a/fabric/src/main/resources/architectury.mixins.json
+++ b/fabric/src/main/resources/architectury.mixins.json
@@ -52,6 +52,7 @@
"MixinPhantomSpawner",
"MixinPlayer",
"MixinPlayerAdvancements",
+ "MixinPlayerChunkSender",
"MixinPlayerList",
"MixinResultSlot",
"MixinServerLevel",
diff --git a/minecraftforge/src/main/java/dev/architectury/mixin/forge/minecraftforge/MixinChunkMap.java b/minecraftforge/src/main/java/dev/architectury/mixin/forge/minecraftforge/MixinChunkMap.java
new file mode 100644
index 00000000..e68cd47a
--- /dev/null
+++ b/minecraftforge/src/main/java/dev/architectury/mixin/forge/minecraftforge/MixinChunkMap.java
@@ -0,0 +1,43 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021, 2022 architectury
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package dev.architectury.mixin.forge.minecraftforge;
+
+import dev.architectury.event.events.common.ChunkWatchEvent;
+import net.minecraft.server.level.ChunkMap;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.chunk.LevelChunk;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(ChunkMap.class)
+public abstract class MixinChunkMap {
+ @Inject(method = "markChunkPendingToSend(Lnet/minecraft/server/level/ServerPlayer;Lnet/minecraft/world/level/chunk/LevelChunk;)V", at = @At("TAIL"))
+ private static void watch(ServerPlayer player, LevelChunk chunk, CallbackInfo ci) {
+ ChunkWatchEvent.WATCH.invoker().listen(chunk, player.serverLevel(), player);
+ }
+
+ @Inject(method = "dropChunk(Lnet/minecraft/server/level/ServerPlayer;Lnet/minecraft/world/level/ChunkPos;)V", at = @At("HEAD"))
+ private static void unwatch(ServerPlayer player, ChunkPos chunkPos, CallbackInfo ci) {
+ ChunkWatchEvent.UNWATCH.invoker().listen(chunkPos, player.serverLevel(), player);
+ }
+}
diff --git a/minecraftforge/src/main/java/dev/architectury/mixin/forge/minecraftforge/MixinPlayerChunkSender.java b/minecraftforge/src/main/java/dev/architectury/mixin/forge/minecraftforge/MixinPlayerChunkSender.java
new file mode 100644
index 00000000..81faaade
--- /dev/null
+++ b/minecraftforge/src/main/java/dev/architectury/mixin/forge/minecraftforge/MixinPlayerChunkSender.java
@@ -0,0 +1,38 @@
+/*
+ * This file is part of architectury.
+ * Copyright (C) 2020, 2021, 2022 architectury
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package dev.architectury.mixin.forge.minecraftforge;
+
+import dev.architectury.event.events.common.ChunkWatchEvent;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.network.PlayerChunkSender;
+import net.minecraft.server.network.ServerGamePacketListenerImpl;
+import net.minecraft.world.level.chunk.LevelChunk;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(PlayerChunkSender.class)
+public abstract class MixinPlayerChunkSender {
+ @Inject(method = "sendChunk", at = @At("TAIL"))
+ private static void sendChunk(ServerGamePacketListenerImpl packetListener, ServerLevel level, LevelChunk chunk, CallbackInfo ci) {
+ ChunkWatchEvent.SENT.invoker().listen(chunk, level, packetListener.player);
+ }
+}
diff --git a/minecraftforge/src/main/resources/architectury-forge.mixins.json b/minecraftforge/src/main/resources/architectury-forge.mixins.json
index a95e797e..b9815374 100644
--- a/minecraftforge/src/main/resources/architectury-forge.mixins.json
+++ b/minecraftforge/src/main/resources/architectury-forge.mixins.json
@@ -6,6 +6,8 @@
"client": [
],
"mixins": [
+ "minecraftforge.MixinChunkMap",
+ "minecraftforge.MixinPlayerChunkSender",
"minecraftforge.MixinChunkSerializer"
],
"injectors": {
diff --git a/neoforge/src/main/java/dev/architectury/neoforge/ArchitecturyNeoForge.java b/neoforge/src/main/java/dev/architectury/neoforge/ArchitecturyNeoForge.java
index a07507d4..31bc61e7 100644
--- a/neoforge/src/main/java/dev/architectury/neoforge/ArchitecturyNeoForge.java
+++ b/neoforge/src/main/java/dev/architectury/neoforge/ArchitecturyNeoForge.java
@@ -20,8 +20,10 @@
package dev.architectury.neoforge;
import dev.architectury.event.EventHandler;
+import dev.architectury.event.events.common.ChunkWatchEvent;
import dev.architectury.registry.level.biome.forge.BiomeModificationsImpl;
import dev.architectury.utils.ArchitecturyConstants;
+import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.Mod;
@Mod(ArchitecturyConstants.MOD_ID)
@@ -30,4 +32,22 @@ public ArchitecturyNeoForge() {
EventHandler.init();
BiomeModificationsImpl.init();
}
+
+ @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.FORGE)
+ private static class ForgeBusSubscriber {
+ @SubscribeEvent
+ private static void event(net.neoforged.neoforge.event.level.ChunkWatchEvent.Watch event) {
+ ChunkWatchEvent.WATCH.invoker().listen(event.getChunk(), event.getLevel(), event.getPlayer());
+ }
+
+ @SubscribeEvent
+ private static void event(net.neoforged.neoforge.event.level.ChunkWatchEvent.Sent event) {
+ ChunkWatchEvent.SENT.invoker().listen(event.getChunk(), event.getLevel(), event.getPlayer());
+ }
+
+ @SubscribeEvent
+ private static void event(net.neoforged.neoforge.event.level.ChunkWatchEvent.UnWatch event) {
+ ChunkWatchEvent.UNWATCH.invoker().listen(event.getPos(), event.getLevel(), event.getPlayer());
+ }
+ }
}
diff --git a/testmod-common/src/main/java/dev/architectury/test/events/DebugEvents.java b/testmod-common/src/main/java/dev/architectury/test/events/DebugEvents.java
index 5423bf98..14ad5468 100644
--- a/testmod-common/src/main/java/dev/architectury/test/events/DebugEvents.java
+++ b/testmod-common/src/main/java/dev/architectury/test/events/DebugEvents.java
@@ -235,6 +235,15 @@ public static void debugEvents() {
ChunkEvent.SAVE_DATA.register((chunk, level, nbt) -> {
// TestMod.SINK.accept("Chunk saved at x=" + chunk.getPos().x + ", z=" + chunk.getPos().z + " in dimension '" + level.dimension().location() + "'");
});
+ ChunkWatchEvent.WATCH.register((chunk, level, player) -> {
+// TestMod.SINK.accept("Chunk at x=%d, z=%d in dimension '%s' being watched by %s", chunk.getPos().x, chunk.getPos().z, level.dimension().location(), player.getScoreboardName());
+ });
+ ChunkWatchEvent.SENT.register((chunk, level, player) -> {
+// TestMod.SINK.accept("Chunk at x=%d, z=%d in dimension '%s' sent to %s", chunk.getPos().x, chunk.getPos().z, level.dimension().location(), player.getScoreboardName());
+ });
+ ChunkWatchEvent.UNWATCH.register((chunkPos, level, player) -> {
+// TestMod.SINK.accept("Chunk at x=%d, z=%d in dimension '%s' abandoned by %s", chunkPos.x, chunkPos.z, level.dimension().location(), player.getScoreboardName());
+ });
}
public static String toShortString(Vec3i pos) {