diff --git a/lifecycle-events-common/src/main/java/band/kessoku/lib/events/lifecycle/api/client/ClientBlockEntityEvent.java b/lifecycle-events-common/src/main/java/band/kessoku/lib/events/lifecycle/api/client/ClientBlockEntityEvent.java
new file mode 100644
index 00000000..cc5e2abc
--- /dev/null
+++ b/lifecycle-events-common/src/main/java/band/kessoku/lib/events/lifecycle/api/client/ClientBlockEntityEvent.java
@@ -0,0 +1,42 @@
+package band.kessoku.lib.events.lifecycle.api.client;
+
+import band.kessoku.lib.event.api.Event;
+
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.client.world.ClientWorld;
+
+public class ClientBlockEntityEvent {
+
+ /**
+ * Called when a BlockEntity is loaded into a ClientWorld.
+ *
+ *
When this event is called, the block entity is already in the world.
+ * However, its data might not be loaded yet, so don't rely on it.
+ */
+ public static final Event LOADED = Event.of(loadeds -> (blockEntity, world) -> {
+ for (Loaded loaded : loadeds) {
+ loaded.onLoaded(blockEntity, world);
+ }
+ });
+
+ /**
+ * Called when a BlockEntity is about to be unloaded from a ClientWorld.
+ *
+ * When this event is called, the block entity is still present on the world.
+ */
+ public static final Event UNLOADED = Event.of(unloadeds -> (blockEntity, world) -> {
+ for (Unloaded unloaded : unloadeds) {
+ unloaded.onUnloaded(blockEntity, world);
+ }
+ });
+
+ @FunctionalInterface
+ public interface Loaded {
+ void onLoaded(BlockEntity blockEntity, ClientWorld world);
+ }
+
+ @FunctionalInterface
+ public interface Unloaded {
+ void onUnloaded(BlockEntity blockEntity, ClientWorld world);
+ }
+}
diff --git a/lifecycle-events-common/src/main/java/band/kessoku/lib/events/lifecycle/api/client/ClientChunkEvent.java b/lifecycle-events-common/src/main/java/band/kessoku/lib/events/lifecycle/api/client/ClientChunkEvent.java
new file mode 100644
index 00000000..20d369ca
--- /dev/null
+++ b/lifecycle-events-common/src/main/java/band/kessoku/lib/events/lifecycle/api/client/ClientChunkEvent.java
@@ -0,0 +1,41 @@
+package band.kessoku.lib.events.lifecycle.api.client;
+
+import band.kessoku.lib.event.api.Event;
+
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.world.chunk.WorldChunk;
+
+public class ClientChunkEvent {
+
+ /**
+ * Called when a chunk is loaded into a ClientWorld.
+ *
+ * When this event is called, the chunk is already in the world.
+ */
+ public static final Event LOADED = Event.of(loadeds -> (clientWorld, chunk) -> {
+ for (Loaded callback : loadeds) {
+ callback.onChunkLoaded(clientWorld, chunk);
+ }
+ });
+
+ /**
+ * Called when a chunk is about to be unloaded from a ClientWorld.
+ *
+ * When this event is called, the chunk is still present in the world.
+ */
+ public static final Event UNLOADED = Event.of(unloadeds -> (clientWorld, chunk) -> {
+ for (Unloaded unloaded : unloadeds) {
+ unloaded.onChunkUnloaded(clientWorld, chunk);
+ }
+ });
+
+ @FunctionalInterface
+ public interface Loaded {
+ void onChunkLoaded(ClientWorld world, WorldChunk chunk);
+ }
+
+ @FunctionalInterface
+ public interface Unloaded {
+ void onChunkUnloaded(ClientWorld world, WorldChunk chunk);
+ }
+}
diff --git a/lifecycle-events-common/src/main/java/band/kessoku/lib/events/lifecycle/api/client/ClientEntityEvent.java b/lifecycle-events-common/src/main/java/band/kessoku/lib/events/lifecycle/api/client/ClientEntityEvent.java
new file mode 100644
index 00000000..513344cd
--- /dev/null
+++ b/lifecycle-events-common/src/main/java/band/kessoku/lib/events/lifecycle/api/client/ClientEntityEvent.java
@@ -0,0 +1,41 @@
+package band.kessoku.lib.events.lifecycle.api.client;
+
+import band.kessoku.lib.event.api.Event;
+
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.entity.Entity;
+
+public class ClientEntityEvent {
+
+ /**
+ * Called when an Entity is loaded into a ClientWorld.
+ *
+ * When this event is called, the chunk is already in the world.
+ */
+ public static final Event LOADED = Event.of(loadeds -> (entity, world) -> {
+ for (Loaded loaded : loadeds) {
+ loaded.onLoaded(entity, world);
+ }
+ });
+
+ /**
+ * Called when an Entity is about to be unloaded from a ClientWorld.
+ *
+ * This event is called before the entity is unloaded from the world.
+ */
+ public static final Event UNLOADED = Event.of(unloadeds -> (entity, world) -> {
+ for (Unloaded unloaded : unloadeds) {
+ unloaded.onUnloaded(entity, world);
+ }
+ });
+
+ @FunctionalInterface
+ public interface Loaded {
+ void onLoaded(Entity entity, ClientWorld world);
+ }
+
+ @FunctionalInterface
+ public interface Unloaded {
+ void onUnloaded(Entity entity, ClientWorld world);
+ }
+}
diff --git a/lifecycle-events-common/src/main/java/band/kessoku/lib/events/lifecycle/api/client/ClientTickEvent.java b/lifecycle-events-common/src/main/java/band/kessoku/lib/events/lifecycle/api/client/ClientTickEvent.java
index 1045c0bd..1a60b556 100644
--- a/lifecycle-events-common/src/main/java/band/kessoku/lib/events/lifecycle/api/client/ClientTickEvent.java
+++ b/lifecycle-events-common/src/main/java/band/kessoku/lib/events/lifecycle/api/client/ClientTickEvent.java
@@ -68,6 +68,4 @@ interface End {
void onEndTick(ClientWorld world);
}
}
-
-
}
diff --git a/lifecycle-events-fabric/src/main/java/band/kessoku/lib/events/lifecycle/impl/KessokuLifecycleEventsImplFabric.java b/lifecycle-events-fabric/src/main/java/band/kessoku/lib/events/lifecycle/impl/KessokuLifecycleEventsImplFabric.java
index 1adb4a96..15fd85b4 100644
--- a/lifecycle-events-fabric/src/main/java/band/kessoku/lib/events/lifecycle/impl/KessokuLifecycleEventsImplFabric.java
+++ b/lifecycle-events-fabric/src/main/java/band/kessoku/lib/events/lifecycle/impl/KessokuLifecycleEventsImplFabric.java
@@ -1,11 +1,9 @@
package band.kessoku.lib.events.lifecycle.impl;
import band.kessoku.lib.events.lifecycle.api.*;
-import band.kessoku.lib.events.lifecycle.api.client.ClientLifecycleEvent;
-import band.kessoku.lib.events.lifecycle.api.client.ClientTickEvent;
+import band.kessoku.lib.events.lifecycle.api.client.*;
-import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
-import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.*;
import net.fabricmc.fabric.api.event.lifecycle.v1.*;
public class KessokuLifecycleEventsImplFabric {
@@ -19,6 +17,15 @@ public static void registerClientEvents() {
ClientTickEvents.END_CLIENT_TICK.register(client -> ClientTickEvent.END_CLIENT_TICK.invoker().onEndTick(client));
ClientTickEvents.START_WORLD_TICK.register(world -> ClientTickEvent.START_WORLD_TICK.invoker().onStartTick(world));
ClientTickEvents.END_WORLD_TICK.register(world -> ClientTickEvent.END_WORLD_TICK.invoker().onEndTick(world));
+
+ ClientEntityEvents.ENTITY_LOAD.register((entity, world) -> ClientEntityEvent.LOADED.invoker().onLoaded(entity, world));
+ ClientEntityEvents.ENTITY_UNLOAD.register((entity, world) -> ClientEntityEvent.UNLOADED.invoker().onUnloaded(entity, world));
+
+ ClientChunkEvents.CHUNK_LOAD.register((world, chunk) -> ClientChunkEvent.LOADED.invoker().onChunkLoaded(world, chunk));
+ ClientChunkEvents.CHUNK_UNLOAD.register((world, chunk) -> ClientChunkEvent.UNLOADED.invoker().onChunkUnloaded(world, chunk));
+
+ ClientBlockEntityEvents.BLOCK_ENTITY_LOAD.register((blockEntity, world) -> ClientBlockEntityEvent.LOADED.invoker().onLoaded(blockEntity, world));
+ ClientBlockEntityEvents.BLOCK_ENTITY_UNLOAD.register((blockEntity, world) -> ClientBlockEntityEvent.UNLOADED.invoker().onUnloaded(blockEntity, world));
}
public static void registerCommonEvents() {
diff --git a/lifecycle-events-neo/build.gradle b/lifecycle-events-neo/build.gradle
index a47865b0..8226ca98 100644
--- a/lifecycle-events-neo/build.gradle
+++ b/lifecycle-events-neo/build.gradle
@@ -14,6 +14,12 @@ architectury {
neoForge()
}
+loom {
+ neoForge {
+ setAccessTransformers(files("src/main/resources/META-INF/accesstransformer.cfg"))
+ }
+}
+
repositories {
maven { url "https://maven.neoforged.net/releases/" }
}
diff --git a/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/KessokuLifecycleEventsEntrypoint.java b/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/KessokuLifecycleEventsEntrypoint.java
index d430d856..287388a9 100644
--- a/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/KessokuLifecycleEventsEntrypoint.java
+++ b/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/KessokuLifecycleEventsEntrypoint.java
@@ -13,10 +13,9 @@ public class KessokuLifecycleEventsEntrypoint {
public KessokuLifecycleEventsEntrypoint(IEventBus modEventBus, ModContainer modContainer) {
var forgeEventBus = NeoForge.EVENT_BUS;
- KessokuLifecycleEventsImplNeo.registerCommonEvents(modEventBus, forgeEventBus);
-
+ KessokuLifecycleEventsImplNeo.registerCommonEvents(forgeEventBus);
if (FMLLoader.getDist().isClient()) {
- KessokuLifecycleEventsImplNeo.registerClientEvents(modEventBus, forgeEventBus);
+ KessokuLifecycleEventsImplNeo.registerClientEvents(forgeEventBus);
}
}
}
diff --git a/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/impl/KessokuLifecycleEventsImplNeo.java b/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/impl/KessokuLifecycleEventsImplNeo.java
index 7ce9558a..5e0be43a 100644
--- a/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/impl/KessokuLifecycleEventsImplNeo.java
+++ b/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/impl/KessokuLifecycleEventsImplNeo.java
@@ -3,16 +3,19 @@
import band.kessoku.lib.event.util.NeoEventUtils;
import band.kessoku.lib.events.lifecycle.api.*;
+import band.kessoku.lib.events.lifecycle.api.client.ClientChunkEvent;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
+import net.minecraft.world.chunk.WorldChunk;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.client.event.ClientTickEvent;
import net.neoforged.neoforge.event.OnDatapackSyncEvent;
import net.neoforged.neoforge.event.TagsUpdatedEvent;
import net.neoforged.neoforge.event.entity.living.LivingEquipmentChangeEvent;
+import net.neoforged.neoforge.event.level.ChunkEvent;
import net.neoforged.neoforge.event.level.LevelEvent;
import net.neoforged.neoforge.event.server.ServerStartedEvent;
import net.neoforged.neoforge.event.server.ServerStartingEvent;
@@ -21,9 +24,20 @@
import net.neoforged.neoforge.event.tick.LevelTickEvent;
public class KessokuLifecycleEventsImplNeo {
- public static void registerClientEvents(IEventBus modEventBus, IEventBus forgeEventBus) {
+ public static void registerClientEvents(IEventBus forgeEventBus) {
KessokuLifecycleEventsImpl.clientInit();
+ NeoEventUtils.registerEvent(forgeEventBus, ChunkEvent.Load.class, event -> {
+ if (event.getLevel() instanceof ClientWorld world && event.getChunk() instanceof WorldChunk chunk) {
+ ClientChunkEvent.LOADED.invoker().onChunkLoaded(world, chunk);
+ }
+ });
+ NeoEventUtils.registerEvent(forgeEventBus, ChunkEvent.Unload.class, event -> {
+ if (event.getLevel() instanceof ClientWorld world && event.getChunk() instanceof WorldChunk chunk) {
+ ClientChunkEvent.UNLOADED.invoker().onChunkUnloaded(world, chunk);
+ }
+ });
+
NeoEventUtils.registerEvent(forgeEventBus, ClientTickEvent.Pre.class, event -> {
band.kessoku.lib.events.lifecycle.api.client.ClientTickEvent.START_CLIENT_TICK.invoker().onStartTick(MinecraftClient.getInstance());
});
@@ -43,13 +57,24 @@ public static void registerClientEvents(IEventBus modEventBus, IEventBus forgeEv
});
}
- public static void registerCommonEvents(IEventBus modEventBus, IEventBus forgeEventBus) {
+ public static void registerCommonEvents(IEventBus forgeEventBus) {
KessokuLifecycleEventsImpl.init();
NeoEventUtils.registerEvent(forgeEventBus, TagsUpdatedEvent.class, event -> {
LifecycleEvent.TAG_LOADED.invoker().onTagsLoaded(event.getRegistryAccess(), event.getUpdateCause() == TagsUpdatedEvent.UpdateCause.CLIENT_PACKET_RECEIVED);
});
+ NeoEventUtils.registerEvent(forgeEventBus, ChunkEvent.Load.class, event -> {
+ if (event.getLevel() instanceof ServerWorld world && event.getChunk() instanceof WorldChunk chunk) {
+ ServerChunkEvent.LOADED.invoker().onChunkLoaded(world, chunk);
+ }
+ });
+ NeoEventUtils.registerEvent(forgeEventBus, ChunkEvent.Unload.class, event -> {
+ if (event.getLevel() instanceof ServerWorld world && event.getChunk() instanceof WorldChunk chunk) {
+ ServerChunkEvent.UNLOADED.invoker().onChunkUnloaded(world, chunk);
+ }
+ });
+
NeoEventUtils.registerEvent(forgeEventBus, ServerStartingEvent.class, event -> {
ServerLifecycleEvent.STARTING.invoker().onServerStarting(event.getServer());
});
diff --git a/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/mixin/neo/client/ClientChunkManagerMixin.java b/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/mixin/neo/client/ClientChunkManagerMixin.java
new file mode 100644
index 00000000..cc4a375b
--- /dev/null
+++ b/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/mixin/neo/client/ClientChunkManagerMixin.java
@@ -0,0 +1,50 @@
+package band.kessoku.lib.events.lifecycle.mixin.neo.client;
+
+import java.util.function.Consumer;
+
+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;
+
+import net.minecraft.client.world.ClientChunkManager;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.packet.s2c.play.ChunkData;
+import net.minecraft.util.math.ChunkPos;
+import net.minecraft.world.chunk.WorldChunk;
+
+import band.kessoku.lib.events.lifecycle.api.client.ClientChunkEvent;
+
+@Mixin(ClientChunkManager.class)
+public abstract class ClientChunkManagerMixin {
+ @Final
+ @Shadow
+ private ClientWorld world;
+
+ @Inject(method = "loadChunkFromPacket", at = @At(value = "NEW", target = "net/minecraft/world/chunk/WorldChunk", shift = At.Shift.BEFORE), locals = LocalCapture.CAPTURE_FAILHARD)
+ private void onChunkUnload(int x, int z, PacketByteBuf buf, NbtCompound tag, Consumer consumer, CallbackInfoReturnable info, int index, WorldChunk worldChunk, ChunkPos chunkPos) {
+ if (worldChunk != null) {
+ ClientChunkEvent.UNLOADED.invoker().onChunkUnloaded(this.world, worldChunk);
+ }
+ }
+
+ @Inject(
+ method = "updateLoadDistance",
+ at = @At(
+ value = "INVOKE",
+ target = "net/minecraft/client/world/ClientChunkManager$ClientChunkMap.isInRadius(II)Z"
+ ),
+ locals = LocalCapture.CAPTURE_FAILHARD
+ )
+ private void onUpdateLoadDistance(int loadDistance, CallbackInfo ci, int oldRadius, int newRadius, ClientChunkManager.ClientChunkMap clientChunkMap, int k, WorldChunk oldChunk, ChunkPos chunkPos) {
+ if (!clientChunkMap.isInRadius(chunkPos.x, chunkPos.z)) {
+ ClientChunkEvent.UNLOADED.invoker().onChunkUnloaded(this.world, oldChunk);
+ }
+ }
+}
diff --git a/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/mixin/neo/client/ClientEntityHandlerMixin.java b/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/mixin/neo/client/ClientEntityHandlerMixin.java
new file mode 100644
index 00000000..f749e470
--- /dev/null
+++ b/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/mixin/neo/client/ClientEntityHandlerMixin.java
@@ -0,0 +1,34 @@
+package band.kessoku.lib.events.lifecycle.mixin.neo.client;
+
+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 net.minecraft.client.world.ClientWorld;
+import net.minecraft.entity.Entity;
+
+import band.kessoku.lib.events.lifecycle.api.client.ClientEntityEvent;
+
+@Mixin(targets = "net/minecraft/client/world/ClientWorld$ClientEntityHandler")
+public class ClientEntityHandlerMixin {
+ // final synthetic Lnet/minecraft/client/world/ClientWorld; field_27735
+ @SuppressWarnings("ShadowTarget")
+ @Shadow
+ @Final
+ private ClientWorld field_27735;
+
+ // Call our load event after vanilla has loaded the entity
+ @Inject(method = "startTracking(Lnet/minecraft/entity/Entity;)V", at = @At("TAIL"))
+ private void invokeLoadEntity(Entity entity, CallbackInfo ci) {
+ ClientEntityEvent.LOADED.invoker().onLoaded(entity, this.field_27735);
+ }
+
+ // Call our unload event before vanilla does.
+ @Inject(method = "stopTracking(Lnet/minecraft/entity/Entity;)V", at = @At("HEAD"))
+ private void invokeUnloadEntity(Entity entity, CallbackInfo ci) {
+ ClientEntityEvent.UNLOADED.invoker().onUnloaded(entity, this.field_27735);
+ }
+}
diff --git a/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/mixin/neo/client/ClientPlayNetworkHandlerMixin.java b/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/mixin/neo/client/ClientPlayNetworkHandlerMixin.java
new file mode 100644
index 00000000..3865e5dd
--- /dev/null
+++ b/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/mixin/neo/client/ClientPlayNetworkHandlerMixin.java
@@ -0,0 +1,80 @@
+package band.kessoku.lib.events.lifecycle.mixin.neo.client;
+
+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 net.minecraft.block.entity.BlockEntity;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.entity.Entity;
+import net.minecraft.network.packet.s2c.play.GameJoinS2CPacket;
+import net.minecraft.network.packet.s2c.play.PlayerRespawnS2CPacket;
+import net.minecraft.world.chunk.WorldChunk;
+
+import band.kessoku.lib.events.lifecycle.api.client.ClientBlockEntityEvent;
+import band.kessoku.lib.events.lifecycle.api.client.ClientEntityEvent;
+import band.kessoku.lib.events.lifecycle.impl.LoadedChunksCache;
+
+@Mixin(ClientPlayNetworkHandler.class)
+abstract class ClientPlayNetworkHandlerMixin {
+ @Shadow
+ private ClientWorld world;
+
+ @Inject(method = "onPlayerRespawn", at = @At(value = "NEW", target = "net/minecraft/client/world/ClientWorld"))
+ private void onPlayerRespawn(PlayerRespawnS2CPacket packet, CallbackInfo ci) {
+ // If a world already exists, we need to unload all (block)entities in the world.
+ if (this.world != null) {
+ for (Entity entity : this.world.getEntities()) {
+ ClientEntityEvent.UNLOADED.invoker().onUnloaded(entity, this.world);
+ }
+
+ for (WorldChunk chunk : ((LoadedChunksCache) this.world).kessoku$getLoadedChunks()) {
+ for (BlockEntity blockEntity : chunk.getBlockEntities().values()) {
+ ClientBlockEntityEvent.UNLOADED.invoker().onUnloaded(blockEntity, this.world);
+ }
+ }
+ }
+ }
+
+ /**
+ * An explanation why we unload entities during onGameJoin:
+ * Proxies such as Waterfall may send another Game Join packet if entity meta rewrite is disabled, so we will cover ourselves.
+ * Velocity by default will send a Game Join packet when the player changes servers, which will create a new client world.
+ * Also anyone can send another GameJoinPacket at any time, so we need to watch out.
+ */
+ @Inject(method = "onGameJoin", at = @At(value = "NEW", target = "net/minecraft/client/world/ClientWorld"))
+ private void onGameJoin(GameJoinS2CPacket packet, CallbackInfo ci) {
+ // If a world already exists, we need to unload all (block)entities in the world.
+ if (this.world != null) {
+ for (Entity entity : world.getEntities()) {
+ ClientEntityEvent.UNLOADED.invoker().onUnloaded(entity, this.world);
+ }
+
+ for (WorldChunk chunk : ((LoadedChunksCache) this.world).kessoku$getLoadedChunks()) {
+ for (BlockEntity blockEntity : chunk.getBlockEntities().values()) {
+ ClientBlockEntityEvent.UNLOADED.invoker().onUnloaded(blockEntity, this.world);
+ }
+ }
+ }
+ }
+
+ // Called when the client disconnects from a server or enters reconfiguration.
+ @Inject(method = "clearWorld", at = @At("HEAD"))
+ private void onClearWorld(CallbackInfo ci) {
+ // If a world already exists, we need to unload all (block)entities in the world.
+ if (this.world != null) {
+ for (Entity entity : this.world.getEntities()) {
+ ClientEntityEvent.UNLOADED.invoker().onUnloaded(entity, this.world);
+ }
+
+ for (WorldChunk chunk : ((LoadedChunksCache) this.world).kessoku$getLoadedChunks()) {
+ for (BlockEntity blockEntity : chunk.getBlockEntities().values()) {
+ ClientBlockEntityEvent.UNLOADED.invoker().onUnloaded(blockEntity, this.world);
+ }
+ }
+ }
+ }
+}
diff --git a/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/mixin/neo/client/MinecraftClientMixin.java b/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/mixin/neo/client/MinecraftClientMixin.java
new file mode 100644
index 00000000..da692f9f
--- /dev/null
+++ b/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/mixin/neo/client/MinecraftClientMixin.java
@@ -0,0 +1,24 @@
+package band.kessoku.lib.events.lifecycle.mixin.neo.client;
+
+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;
+
+import net.minecraft.client.MinecraftClient;
+
+import band.kessoku.lib.events.lifecycle.api.client.ClientLifecycleEvent;
+
+@Mixin(MinecraftClient.class)
+public abstract class MinecraftClientMixin {
+ @Inject(at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;info(Ljava/lang/String;)V", shift = At.Shift.AFTER, remap = false), method = "stop")
+ private void onStopping(CallbackInfo ci) {
+ ClientLifecycleEvent.STOPPING.invoker().onClientStopping((MinecraftClient) (Object) this);
+ }
+
+ // We inject after the thread field is set so `ThreadExecutor#getThread` will work
+ @Inject(at = @At(value = "FIELD", target = "Lnet/minecraft/client/MinecraftClient;thread:Ljava/lang/Thread;", shift = At.Shift.AFTER, ordinal = 0), method = "run")
+ private void onStart(CallbackInfo ci) {
+ ClientLifecycleEvent.STARTED.invoker().onClientStarted((MinecraftClient) (Object) this);
+ }
+}
diff --git a/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/mixin/neo/client/WorldChunkMixin.java b/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/mixin/neo/client/WorldChunkMixin.java
new file mode 100644
index 00000000..1a509b7f
--- /dev/null
+++ b/lifecycle-events-neo/src/main/java/band/kessoku/lib/events/lifecycle/mixin/neo/client/WorldChunkMixin.java
@@ -0,0 +1,94 @@
+package band.kessoku.lib.events.lifecycle.mixin.neo.client;
+
+import java.util.Map;
+
+import com.llamalad7.mixinextras.sugar.Local;
+import org.jetbrains.annotations.Nullable;
+import org.objectweb.asm.Opcodes;
+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.Redirect;
+import org.spongepowered.asm.mixin.injection.Slice;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
+
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.server.world.ServerWorld;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+import net.minecraft.world.chunk.WorldChunk;
+
+import band.kessoku.lib.events.lifecycle.api.ServerBlockEntityEvent;
+import band.kessoku.lib.events.lifecycle.api.client.ClientBlockEntityEvent;
+
+@Mixin(WorldChunk.class)
+abstract class WorldChunkMixin {
+ @Shadow
+ public abstract World getWorld();
+
+ @Inject(method = "setBlockEntity", at = @At(value = "INVOKE", target = "Ljava/util/Map;put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", shift = At.Shift.BY, by = 3))
+ private void onLoadBlockEntity(BlockEntity blockEntity, CallbackInfo ci, @Local(ordinal = 1) BlockEntity removedBlockEntity) {
+ // Only fire the load event if the block entity has actually changed
+ if (blockEntity != null && blockEntity != removedBlockEntity) {
+ if (this.getWorld() instanceof ServerWorld) {
+ ServerBlockEntityEvent.LOADED.invoker().onLoaded(blockEntity, (ServerWorld) this.getWorld());
+ } else if (this.getWorld() instanceof ClientWorld) {
+ ClientBlockEntityEvent.LOADED.invoker().onLoaded(blockEntity, (ClientWorld) this.getWorld());
+ }
+ }
+ }
+
+ @Inject(method = "setBlockEntity", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/entity/BlockEntity;markRemoved()V", shift = At.Shift.AFTER))
+ private void onRemoveBlockEntity(BlockEntity blockEntity, CallbackInfo info, @Local(ordinal = 1) BlockEntity removedBlockEntity) {
+ if (removedBlockEntity != null) {
+ if (this.getWorld() instanceof ServerWorld) {
+ ServerBlockEntityEvent.UNLOADED.invoker().onUnloaded(removedBlockEntity, (ServerWorld) this.getWorld());
+ } else if (this.getWorld() instanceof ClientWorld) {
+ ClientBlockEntityEvent.UNLOADED.invoker().onUnloaded(removedBlockEntity, (ClientWorld) this.getWorld());
+ }
+ }
+ }
+
+ // Use the slice to not redirect codepath where block entity is loaded
+ @Redirect(
+ method = "getBlockEntity(Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/world/chunk/WorldChunk$CreationType;)Lnet/minecraft/block/entity/BlockEntity;",
+ at = @At(
+ value = "INVOKE",
+ target = "Ljava/util/Map;remove(Ljava/lang/Object;)Ljava/lang/Object;"
+ ),
+ slice = @Slice(
+ to = @At(
+ value = "FIELD",
+ target = "Lnet/minecraft/world/chunk/WorldChunk;blockEntityNbts:Ljava/util/Map;",
+ opcode = Opcodes.GETFIELD
+ )
+ )
+ )
+ private Object onRemoveBlockEntity(Map map, K key) {
+ @Nullable final V removed = map.remove(key);
+
+ if (removed != null) {
+ if (this.getWorld() instanceof ServerWorld) {
+ ServerBlockEntityEvent.UNLOADED.invoker().onUnloaded((BlockEntity) removed, (ServerWorld) this.getWorld());
+ } else if (this.getWorld() instanceof ClientWorld) {
+ ClientBlockEntityEvent.UNLOADED.invoker().onUnloaded((BlockEntity) removed, (ClientWorld) this.getWorld());
+ }
+ }
+
+ return removed;
+ }
+
+ @Inject(method = "removeBlockEntity", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/entity/BlockEntity;markRemoved()V"), locals = LocalCapture.CAPTURE_FAILEXCEPTION)
+ private void onRemoveBlockEntity(BlockPos pos, CallbackInfo ci, @Nullable BlockEntity removed) {
+ if (removed != null) {
+ if (this.getWorld() instanceof ServerWorld) {
+ ServerBlockEntityEvent.UNLOADED.invoker().onUnloaded(removed, (ServerWorld) this.getWorld());
+ } else if (this.getWorld() instanceof ClientWorld) {
+ ClientBlockEntityEvent.UNLOADED.invoker().onUnloaded(removed, (ClientWorld) this.getWorld());
+ }
+ }
+ }
+}
diff --git a/lifecycle-events-neo/src/main/resources/META-INF/accesstransformer.cfg b/lifecycle-events-neo/src/main/resources/META-INF/accesstransformer.cfg
new file mode 100644
index 00000000..11e2bad8
--- /dev/null
+++ b/lifecycle-events-neo/src/main/resources/META-INF/accesstransformer.cfg
@@ -0,0 +1,3 @@
+public net.minecraft.server.MinecraftServer$ReloadableResources
+public net.minecraft.client.multiplayer.ClientChunkCache$Storage
+public net.minecraft.client.multiplayer.ClientChunkCache$Storage inRange(II)Z # inRange
\ No newline at end of file
diff --git a/lifecycle-events-neo/src/main/resources/META-INF/neoforge.mods.toml b/lifecycle-events-neo/src/main/resources/META-INF/neoforge.mods.toml
index 4620df78..dacde038 100644
--- a/lifecycle-events-neo/src/main/resources/META-INF/neoforge.mods.toml
+++ b/lifecycle-events-neo/src/main/resources/META-INF/neoforge.mods.toml
@@ -20,6 +20,9 @@ config = "kessoku-lifecycle-events.mixins.json"
[[mixins]]
config = "kessoku-lifecycle-events.neo.mixins.json"
+[[accessTransformers]]
+file = "META-INF/accesstransformer.cfg"
+
[[dependencies.kessoku-event-base]]
modId = "neoforge"
type = "required"
diff --git a/lifecycle-events-neo/src/main/resources/kessoku-lifecycle-events.neo.mixins.json b/lifecycle-events-neo/src/main/resources/kessoku-lifecycle-events.neo.mixins.json
index 05930534..f5267d43 100644
--- a/lifecycle-events-neo/src/main/resources/kessoku-lifecycle-events.neo.mixins.json
+++ b/lifecycle-events-neo/src/main/resources/kessoku-lifecycle-events.neo.mixins.json
@@ -10,6 +10,13 @@
"server": [
"server.WorldChunkMixin"
],
+ "client": [
+ "client.ClientChunkManagerMixin",
+ "client.ClientPlayNetworkHandlerMixin",
+ "client.ClientEntityHandlerMixin",
+ "client.MinecraftClientMixin",
+ "client.WorldChunkMixin"
+ ],
"injectors": {
"defaultRequire": 1,
"maxShiftBy": 3