From 016c11f826621c866a2cde93b2fba8825b508fd0 Mon Sep 17 00:00:00 2001 From: Xytronix <32957125+Xytronix@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:58:39 +0100 Subject: [PATCH 1/9] Add component caching --- .../service/AiTickThrottlerService.java | 186 +++++++++++------- .../system/AiTickThrottlerCleanupSystem.java | 12 +- 2 files changed, 119 insertions(+), 79 deletions(-) diff --git a/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java b/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java index 0294e00..d8ce71b 100644 --- a/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java +++ b/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java @@ -3,6 +3,7 @@ import cc.irori.refixes.component.TickThrottled; import cc.irori.refixes.config.impl.AiTickThrottlerConfig; import cc.irori.refixes.util.Logs; +import com.hypixel.hytale.component.ComponentType; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.query.Query; @@ -42,6 +43,16 @@ public class AiTickThrottlerService { private static final HytaleLogger LOGGER = Logs.logger(); + // Cached component types — resolved once via resolveComponentTypes() + private ComponentType npcType; + private ComponentType transformType; + private ComponentType uuidType; + private ComponentType frozenType; + private ComponentType stepType; + private ComponentType tickThrottledType; + private ComponentType playerType; + private Query npcQuery; + private final Map worldStates = new ConcurrentHashMap<>(); private ScheduledFuture task; @@ -50,7 +61,9 @@ public void registerService() { task = HytaleServer.SCHEDULED_EXECUTOR.scheduleAtFixedRate( () -> { try { - throttle(); + if (resolveComponentTypes()) { + throttle(); + } } catch (Exception e) { LOGGER.atSevere().withCause(e).log("Error in AI tick throttler"); } @@ -82,25 +95,26 @@ private void unfreezeAllWorld() { } private void unfreezeAll(World world, WorldState state) { + if (!resolveComponentTypes()) { + return; + } Store store = world.getEntityStore().getStore(); - store.forEachEntityParallel( - Query.and(EntityModule.get().getNPCMarkerComponentType(), TransformComponent.getComponentType()), - (index, archetypeChunk, commandBuffer) -> { - UUIDComponent uuid = archetypeChunk.getComponent(index, UUIDComponent.getComponentType()); - if (uuid == null) { - return; - } + store.forEachEntityParallel(npcQuery, (index, archetypeChunk, commandBuffer) -> { + UUIDComponent uuid = archetypeChunk.getComponent(index, uuidType); + if (uuid == null) { + return; + } - AiLodEntry entry = state.entries.get(uuid.getUuid()); - TickThrottled tickThrottled = archetypeChunk.getComponent(index, TickThrottled.getComponentType()); + AiLodEntry entry = state.entries.get(uuid.getUuid()); + TickThrottled tickThrottled = archetypeChunk.getComponent(index, tickThrottledType); - if (entry != null && tickThrottled != null) { - Ref ref = archetypeChunk.getReferenceTo(index); - commandBuffer.tryRemoveComponent(ref, Frozen.getComponentType()); - commandBuffer.tryRemoveComponent(ref, StepComponent.getComponentType()); - commandBuffer.tryRemoveComponent(ref, TickThrottled.getComponentType()); - } - }); + if (entry != null && tickThrottled != null) { + Ref ref = archetypeChunk.getReferenceTo(index); + commandBuffer.tryRemoveComponent(ref, frozenType); + commandBuffer.tryRemoveComponent(ref, stepType); + commandBuffer.tryRemoveComponent(ref, tickThrottledType); + } + }); } private void throttle() { @@ -130,71 +144,66 @@ private void processWorld(World world, AiTickThrottlerConfig cfg) { int midChunks = Math.max(nearChunks, cfg.getValue(AiTickThrottlerConfig.MID_CHUNKS)); int farChunks = Math.max(midChunks, cfg.getValue(AiTickThrottlerConfig.FAR_CHUNKS)); - store.forEachEntityParallel( - Query.and(EntityModule.get().getNPCMarkerComponentType(), TransformComponent.getComponentType()), - (index, archetypeChunk, commandBuffer) -> { - // Skip player entities - if (archetypeChunk.getArchetype().contains(Player.getComponentType())) { - return; - } + store.forEachEntityParallel(npcQuery, (index, archetypeChunk, commandBuffer) -> { + // Skip player entities + if (playerType != null && archetypeChunk.getArchetype().contains(playerType)) { + return; + } - TransformComponent transform = - archetypeChunk.getComponent(index, TransformComponent.getComponentType()); - UUIDComponent uuid = archetypeChunk.getComponent(index, UUIDComponent.getComponentType()); - if (transform == null || uuid == null) { - return; - } + TransformComponent transform = archetypeChunk.getComponent(index, transformType); + UUIDComponent uuid = archetypeChunk.getComponent(index, uuidType); + if (transform == null || uuid == null) { + return; + } - // Compute chunk distance to nearest player - int entityChunkX = - ChunkUtil.chunkCoordinate(transform.getPosition().getX()); - int entityChunkZ = - ChunkUtil.chunkCoordinate(transform.getPosition().getZ()); - int chunkDist = closestPlayerChunkDistance(entityChunkX, entityChunkZ, playerChunks); - UUID entityId = uuid.getUuid(); + // Compute chunk distance to nearest player + int entityChunkX = ChunkUtil.chunkCoordinate(transform.getPosition().getX()); + int entityChunkZ = ChunkUtil.chunkCoordinate(transform.getPosition().getZ()); + int chunkDist = closestPlayerChunkDistance(entityChunkX, entityChunkZ, playerChunks); + UUID entityId = uuid.getUuid(); - double intervalSec = computeInterval(chunkDist, nearChunks, midChunks, farChunks, cfg); + double intervalSec = computeInterval(chunkDist, nearChunks, midChunks, farChunks, cfg); - Ref ref = archetypeChunk.getReferenceTo(index); + Ref ref = archetypeChunk.getReferenceTo(index); - boolean frozen = archetypeChunk.getComponent(index, Frozen.getComponentType()) != null; - boolean throttled = archetypeChunk.getComponent(index, TickThrottled.getComponentType()) != null; + boolean frozen = archetypeChunk.getComponent(index, frozenType) != null; + boolean throttled = archetypeChunk.getComponent(index, tickThrottledType) != null; - // Don't mess around if the entity is already frozen without our throttle marker - if (frozen && !throttled) { - return; - } + // Don't mess around if the entity is already frozen without our throttle marker + if (frozen && !throttled) { + return; + } - // If near enough, remove throttling - if (intervalSec <= 0.0) { - if (throttled) { - commandBuffer.tryRemoveComponent(ref, Frozen.getComponentType()); - commandBuffer.tryRemoveComponent(ref, StepComponent.getComponentType()); - commandBuffer.tryRemoveComponent(ref, TickThrottled.getComponentType()); - } - state.entries.remove(entityId); - return; - } + // If near enough, remove throttling + if (intervalSec <= 0.0) { + if (throttled) { + commandBuffer.tryRemoveComponent(ref, frozenType); + commandBuffer.tryRemoveComponent(ref, stepType); + commandBuffer.tryRemoveComponent(ref, tickThrottledType); + } + state.entries.remove(entityId); + return; + } - AiLodEntry entry = state.entries.computeIfAbsent(uuid.getUuid(), _k -> new AiLodEntry()); - if (!throttled) { - commandBuffer.ensureComponent(ref, Frozen.getComponentType()); - commandBuffer.ensureComponent(ref, TickThrottled.getComponentType()); - } + AiLodEntry entry = state.entries.computeIfAbsent(uuid.getUuid(), _k -> new AiLodEntry()); + if (!throttled) { + commandBuffer.ensureComponent(ref, frozenType); + commandBuffer.ensureComponent(ref, tickThrottledType); + } - float minTick = cfg.getValue(AiTickThrottlerConfig.MIN_TICK_SECONDS); - long intervalNanos = (long) (Math.max(minTick, intervalSec) * 1_000_000_000.0); - if (entry.intervalNanos != intervalNanos) { - entry.intervalNanos = intervalNanos; - entry.nextTickNanos = now; - } + float minTick = cfg.getValue(AiTickThrottlerConfig.MIN_TICK_SECONDS); + long intervalNanos = (long) (Math.max(minTick, intervalSec) * 1_000_000_000.0); + if (entry.intervalNanos != intervalNanos) { + entry.intervalNanos = intervalNanos; + entry.nextTickNanos = now; + } - if (now >= entry.nextTickNanos) { - commandBuffer.putComponent(ref, StepComponent.getComponentType(), new StepComponent((float) - ((double) intervalNanos / 1_000_000_000.0))); - entry.nextTickNanos = now + intervalNanos; - } - }); + if (now >= entry.nextTickNanos) { + commandBuffer.putComponent( + ref, stepType, new StepComponent((float) ((double) intervalNanos / 1_000_000_000.0))); + entry.nextTickNanos = now + intervalNanos; + } + }); } private static double computeInterval( @@ -219,6 +228,7 @@ private static List collectPlayerChunkPositions(Collection pla for (PlayerRef player : players) { if (player == null) continue; Transform transform = player.getTransform(); + if (transform == null) continue; int chunkX = ChunkUtil.chunkCoordinate(transform.getPosition().getX()); int chunkZ = ChunkUtil.chunkCoordinate(transform.getPosition().getZ()); positions.add(new int[] {chunkX, chunkZ}); @@ -241,6 +251,38 @@ private static int closestPlayerChunkDistance(int entityChunkX, int entityChunkZ return minDist; } + private boolean resolveComponentTypes() { + if (npcType != null + && transformType != null + && uuidType != null + && frozenType != null + && stepType != null + && tickThrottledType != null) { + return true; + } + try { + if (npcType == null) npcType = EntityModule.get().getNPCMarkerComponentType(); + if (transformType == null) transformType = TransformComponent.getComponentType(); + if (uuidType == null) uuidType = UUIDComponent.getComponentType(); + if (frozenType == null) frozenType = Frozen.getComponentType(); + if (stepType == null) stepType = StepComponent.getComponentType(); + if (tickThrottledType == null) tickThrottledType = TickThrottled.getComponentType(); + if (playerType == null) playerType = Player.getComponentType(); + + if (npcQuery == null) { + npcQuery = Query.and(npcType, transformType); + } + } catch (Throwable t) { + return false; + } + return npcType != null + && transformType != null + && uuidType != null + && frozenType != null + && stepType != null + && tickThrottledType != null; + } + private static final class WorldState { final Map entries = new ConcurrentHashMap<>(); } diff --git a/plugin/src/main/java/cc/irori/refixes/system/AiTickThrottlerCleanupSystem.java b/plugin/src/main/java/cc/irori/refixes/system/AiTickThrottlerCleanupSystem.java index df6cdc7..3beaeb4 100644 --- a/plugin/src/main/java/cc/irori/refixes/system/AiTickThrottlerCleanupSystem.java +++ b/plugin/src/main/java/cc/irori/refixes/system/AiTickThrottlerCleanupSystem.java @@ -2,23 +2,21 @@ import cc.irori.refixes.component.TickThrottled; import cc.irori.refixes.config.impl.AiTickThrottlerConfig; -import com.hypixel.hytale.component.*; +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.RemoveReason; +import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.query.Query; import com.hypixel.hytale.component.system.RefSystem; import com.hypixel.hytale.server.core.entity.Frozen; import com.hypixel.hytale.server.core.entity.entities.Player; -import com.hypixel.hytale.server.core.modules.entity.EntityModule; -import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import com.hypixel.hytale.server.npc.components.StepComponent; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; public class AiTickThrottlerCleanupSystem extends RefSystem { - - private static final Query QUERY = - Query.and(EntityModule.get().getNPCMarkerComponentType(), TransformComponent.getComponentType()); - @Override public void onEntityAdded( @NonNull Ref ref, From 6a70176b6d35e11c2e8c480c585e5d767c7a4bdf Mon Sep 17 00:00:00 2001 From: Xytronix <32957125+Xytronix@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:35:33 +0100 Subject: [PATCH 2/9] Fixed cleanup inversion for legacy cleanup and memory leak --- .../cc/irori/refixes/service/AiTickThrottlerService.java | 8 ++++++++ .../refixes/system/AiTickThrottlerCleanupSystem.java | 8 +++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java b/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java index d8ce71b..5d70a78 100644 --- a/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java +++ b/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java @@ -25,6 +25,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; @@ -144,6 +145,9 @@ private void processWorld(World world, AiTickThrottlerConfig cfg) { int midChunks = Math.max(nearChunks, cfg.getValue(AiTickThrottlerConfig.MID_CHUNKS)); int farChunks = Math.max(midChunks, cfg.getValue(AiTickThrottlerConfig.FAR_CHUNKS)); + // Track seen entity UUIDs to prune stale entries after iteration + Set seen = ConcurrentHashMap.newKeySet(); + store.forEachEntityParallel(npcQuery, (index, archetypeChunk, commandBuffer) -> { // Skip player entities if (playerType != null && archetypeChunk.getArchetype().contains(playerType)) { @@ -161,6 +165,7 @@ private void processWorld(World world, AiTickThrottlerConfig cfg) { int entityChunkZ = ChunkUtil.chunkCoordinate(transform.getPosition().getZ()); int chunkDist = closestPlayerChunkDistance(entityChunkX, entityChunkZ, playerChunks); UUID entityId = uuid.getUuid(); + seen.add(entityId); double intervalSec = computeInterval(chunkDist, nearChunks, midChunks, farChunks, cfg); @@ -204,6 +209,9 @@ private void processWorld(World world, AiTickThrottlerConfig cfg) { entry.nextTickNanos = now + intervalNanos; } }); + + // Prune entries for entities no longer in the world + state.entries.keySet().retainAll(seen); } private static double computeInterval( diff --git a/plugin/src/main/java/cc/irori/refixes/system/AiTickThrottlerCleanupSystem.java b/plugin/src/main/java/cc/irori/refixes/system/AiTickThrottlerCleanupSystem.java index 3beaeb4..c1f8fcd 100644 --- a/plugin/src/main/java/cc/irori/refixes/system/AiTickThrottlerCleanupSystem.java +++ b/plugin/src/main/java/cc/irori/refixes/system/AiTickThrottlerCleanupSystem.java @@ -32,9 +32,11 @@ public void onEntityAdded( boolean sweep; if (AiTickThrottlerConfig.get().getValue(AiTickThrottlerConfig.LEGACY_CLEANUP)) { - sweep = commandBuffer.getComponent(ref, TickThrottled.getComponentType()) != null - || commandBuffer.getComponent(ref, Frozen.getComponentType()) != null - || commandBuffer.getComponent(ref, StepComponent.getComponentType()) != null; + if (commandBuffer.getComponent(ref, TickThrottled.getComponentType()) != null) { + sweep = false; + sweep = commandBuffer.getComponent(ref, Frozen.getComponentType()) != null + || commandBuffer.getComponent(ref, StepComponent.getComponentType()) != null; + } } else { sweep = commandBuffer.getComponent(ref, TickThrottled.getComponentType()) != null; } From 44699e8ce80b1aaa22d3408ef9490dd2f7a29614 Mon Sep 17 00:00:00 2001 From: Xytronix <32957125+Xytronix@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:46:06 +0100 Subject: [PATCH 3/9] Fix hysterisis and limit frozen/unfrozen state changes to prevent spikes on teleport --- .../config/impl/AiTickThrottlerConfig.java | 9 +++++++- .../service/AiTickThrottlerService.java | 21 +++++++++++++++---- .../system/AiTickThrottlerCleanupSystem.java | 1 + 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/plugin/src/main/java/cc/irori/refixes/config/impl/AiTickThrottlerConfig.java b/plugin/src/main/java/cc/irori/refixes/config/impl/AiTickThrottlerConfig.java index a745819..6dcd0c0 100644 --- a/plugin/src/main/java/cc/irori/refixes/config/impl/AiTickThrottlerConfig.java +++ b/plugin/src/main/java/cc/irori/refixes/config/impl/AiTickThrottlerConfig.java @@ -35,6 +35,11 @@ public class AiTickThrottlerConfig extends Configuration public static final ConfigurationKey LEGACY_CLEANUP = new ConfigurationKey<>("LegacyCleanup", ConfigField.BOOLEAN, false); + public static final ConfigurationKey ACTIVATION_HYSTERESIS_CHUNKS = + new ConfigurationKey<>("ActivationHysteresisChunks", ConfigField.INTEGER, 1); + public static final ConfigurationKey MAX_UNFREEZES_PER_TICK = + new ConfigurationKey<>("MaxUnfreezesPerTick", ConfigField.INTEGER, 10); + private static final AiTickThrottlerConfig INSTANCE = new AiTickThrottlerConfig(); public AiTickThrottlerConfig() { @@ -49,7 +54,9 @@ public AiTickThrottlerConfig() { FAR_TICK_SECONDS, VERY_FAR_TICK_SECONDS, MIN_TICK_SECONDS, - LEGACY_CLEANUP); + LEGACY_CLEANUP, + ACTIVATION_HYSTERESIS_CHUNKS, + MAX_UNFREEZES_PER_TICK); } public static AiTickThrottlerConfig get() { diff --git a/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java b/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java index 5d70a78..b0eee40 100644 --- a/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java +++ b/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java @@ -30,6 +30,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; /** * Distance-based LOD for AI entity ticking @@ -145,6 +146,10 @@ private void processWorld(World world, AiTickThrottlerConfig cfg) { int midChunks = Math.max(nearChunks, cfg.getValue(AiTickThrottlerConfig.MID_CHUNKS)); int farChunks = Math.max(midChunks, cfg.getValue(AiTickThrottlerConfig.FAR_CHUNKS)); + int hysteresis = Math.max(0, cfg.getValue(AiTickThrottlerConfig.ACTIVATION_HYSTERESIS_CHUNKS)); + int maxUnfreezes = Math.max(1, cfg.getValue(AiTickThrottlerConfig.MAX_UNFREEZES_PER_TICK)); + AtomicInteger unfreezeCount = new AtomicInteger(0); + // Track seen entity UUIDs to prune stale entries after iteration Set seen = ConcurrentHashMap.newKeySet(); @@ -167,8 +172,6 @@ private void processWorld(World world, AiTickThrottlerConfig cfg) { UUID entityId = uuid.getUuid(); seen.add(entityId); - double intervalSec = computeInterval(chunkDist, nearChunks, midChunks, farChunks, cfg); - Ref ref = archetypeChunk.getReferenceTo(index); boolean frozen = archetypeChunk.getComponent(index, frozenType) != null; @@ -179,14 +182,24 @@ private void processWorld(World world, AiTickThrottlerConfig cfg) { return; } + // Already throttled → unfreeze at nearChunks (tight) + // Not throttled → freeze at nearChunks + hysteresis (wider) + double intervalSec; + if (throttled) { + intervalSec = computeInterval(chunkDist, nearChunks, midChunks, farChunks, cfg); + } else { + intervalSec = computeInterval( + chunkDist, nearChunks + hysteresis, midChunks + hysteresis, farChunks + hysteresis, cfg); + } + // If near enough, remove throttling if (intervalSec <= 0.0) { - if (throttled) { + if (throttled && unfreezeCount.incrementAndGet() <= maxUnfreezes) { commandBuffer.tryRemoveComponent(ref, frozenType); commandBuffer.tryRemoveComponent(ref, stepType); commandBuffer.tryRemoveComponent(ref, tickThrottledType); + state.entries.remove(entityId); } - state.entries.remove(entityId); return; } diff --git a/plugin/src/main/java/cc/irori/refixes/system/AiTickThrottlerCleanupSystem.java b/plugin/src/main/java/cc/irori/refixes/system/AiTickThrottlerCleanupSystem.java index c1f8fcd..3258856 100644 --- a/plugin/src/main/java/cc/irori/refixes/system/AiTickThrottlerCleanupSystem.java +++ b/plugin/src/main/java/cc/irori/refixes/system/AiTickThrottlerCleanupSystem.java @@ -34,6 +34,7 @@ public void onEntityAdded( if (AiTickThrottlerConfig.get().getValue(AiTickThrottlerConfig.LEGACY_CLEANUP)) { if (commandBuffer.getComponent(ref, TickThrottled.getComponentType()) != null) { sweep = false; + } else { sweep = commandBuffer.getComponent(ref, Frozen.getComponentType()) != null || commandBuffer.getComponent(ref, StepComponent.getComponentType()) != null; } From 656e6c497091f12531d2b22854d0bbf561c25e0e Mon Sep 17 00:00:00 2001 From: Xytronix <32957125+Xytronix@users.noreply.github.com> Date: Thu, 26 Feb 2026 06:22:52 +0100 Subject: [PATCH 4/9] fix: crash and stability fixes with shutdown timeouts --- .../cc/irori/refixes/early/EarlyOptions.java | 3 + .../early/mixin/MixinBlockSectionSafety.java | 50 +++++++++ .../mixin/MixinEntityChunkLoadingSystem.java | 104 ++++++++++++++++++ .../mixin/MixinMarkerAddRemoveSystem.java | 102 ++++++++++++++++- .../mixin/MixinMotionControllerBase.java | 29 +++++ .../refixes/early/mixin/MixinPlayer.java | 42 +++++++ .../mixin/MixinPortalDeviceSummonPage.java | 25 +++++ .../irori/refixes/early/mixin/MixinStore.java | 27 +++++ .../mixin/MixinTurnOffTeleportersSystem.java | 67 +++++++++++ .../refixes/early/mixin/MixinUniverse.java | 9 +- .../irori/refixes/early/mixin/MixinWorld.java | 23 ++++ early/src/main/resources/refixes.mixins.json | 8 +- .../main/java/cc/irori/refixes/Refixes.java | 3 + .../config/impl/ExperimentalConfig.java | 5 +- 14 files changed, 490 insertions(+), 7 deletions(-) create mode 100644 early/src/main/java/cc/irori/refixes/early/mixin/MixinBlockSectionSafety.java create mode 100644 early/src/main/java/cc/irori/refixes/early/mixin/MixinEntityChunkLoadingSystem.java create mode 100644 early/src/main/java/cc/irori/refixes/early/mixin/MixinMotionControllerBase.java create mode 100644 early/src/main/java/cc/irori/refixes/early/mixin/MixinPlayer.java create mode 100644 early/src/main/java/cc/irori/refixes/early/mixin/MixinTurnOffTeleportersSystem.java diff --git a/early/src/main/java/cc/irori/refixes/early/EarlyOptions.java b/early/src/main/java/cc/irori/refixes/early/EarlyOptions.java index 08fe3ee..06b9579 100644 --- a/early/src/main/java/cc/irori/refixes/early/EarlyOptions.java +++ b/early/src/main/java/cc/irori/refixes/early/EarlyOptions.java @@ -34,6 +34,9 @@ public final class EarlyOptions { public static final Value SHARED_INSTANCES_ENABLED = new Value<>(); public static final Value SHARED_INSTANCES_EXCLUDED_PREFIXES = new Value<>(); + /* Corrupt Section Protection */ + public static final Value CORRUPT_SECTION_PROTECTION = new Value<>(); + // Private constructor to prevent instantiation private EarlyOptions() {} diff --git a/early/src/main/java/cc/irori/refixes/early/mixin/MixinBlockSectionSafety.java b/early/src/main/java/cc/irori/refixes/early/mixin/MixinBlockSectionSafety.java new file mode 100644 index 0000000..1f62a72 --- /dev/null +++ b/early/src/main/java/cc/irori/refixes/early/mixin/MixinBlockSectionSafety.java @@ -0,0 +1,50 @@ +package cc.irori.refixes.early.mixin; + +import cc.irori.refixes.early.EarlyOptions; +import cc.irori.refixes.early.util.Logs; +import com.hypixel.hytale.codec.ExtraInfo; +import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.server.core.universe.world.chunk.section.BlockSection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Guards BlockSection.deserialize() against corrupt section data (#14). + * If deserialization fails, the section is left empty rather than crashing the server. + */ +@Mixin(BlockSection.class) +public abstract class MixinBlockSectionSafety { + + @Unique + private static final HytaleLogger refixes$LOGGER = Logs.logger(); + + @Unique + private static final ThreadLocal refixes$WRAPPING = ThreadLocal.withInitial(() -> false); + + @Shadow + public abstract void deserialize(byte[] bytes, ExtraInfo extraInfo); + + @Inject(method = "deserialize([BLcom/hypixel/hytale/codec/ExtraInfo;)V", at = @At("HEAD"), cancellable = true) + private void refixes$safeDeserialize(byte[] bytes, ExtraInfo extraInfo, CallbackInfo ci) { + if (!EarlyOptions.isAvailable() || !EarlyOptions.CORRUPT_SECTION_PROTECTION.get()) { + return; + } + if (refixes$WRAPPING.get()) { + return; + } + ci.cancel(); + refixes$WRAPPING.set(true); + try { + deserialize(bytes, extraInfo); + } catch (Exception e) { + refixes$LOGGER.atWarning().withCause(e).log( + "BlockSection#deserialize(): Corrupt block section data, leaving section empty"); + } finally { + refixes$WRAPPING.set(false); + } + } +} diff --git a/early/src/main/java/cc/irori/refixes/early/mixin/MixinEntityChunkLoadingSystem.java b/early/src/main/java/cc/irori/refixes/early/mixin/MixinEntityChunkLoadingSystem.java new file mode 100644 index 0000000..bfe6ede --- /dev/null +++ b/early/src/main/java/cc/irori/refixes/early/mixin/MixinEntityChunkLoadingSystem.java @@ -0,0 +1,104 @@ +package cc.irori.refixes.early.mixin; + +import cc.irori.refixes.early.util.Logs; +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.Archetype; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.NonTicking; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.server.core.entity.nameplate.Nameplate; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.chunk.EntityChunk; +import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk; +import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore; +import javax.annotation.Nonnull; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Unique; + +/** + * Fixes NPE in EntityChunkLoadingSystem#onComponentRemoved by adding null checks + * for WorldChunk, EntityChunk, entity holders, and TransformComponent. + * Entities with null TransformComponent are skipped and their chunk is marked dirty. + */ +@Mixin(targets = "com.hypixel.hytale.server.core.universe.world.chunk.EntityChunk$EntityChunkLoadingSystem") +public class MixinEntityChunkLoadingSystem { + + @Unique + private static final HytaleLogger refixes$LOGGER = Logs.logger(); + + @Overwrite + public void onComponentRemoved( + @Nonnull Ref ref, + @Nonnull NonTicking component, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer) { + World world = ((ChunkStore) store.getExternalData()).getWorld(); + + WorldChunk worldChunkComponent = (WorldChunk) store.getComponent(ref, WorldChunk.getComponentType()); + if (worldChunkComponent == null) { + return; + } + + EntityChunk entityChunkComponent = (EntityChunk) store.getComponent(ref, EntityChunk.getComponentType()); + if (entityChunkComponent == null) { + return; + } + + Store entityStore = world.getEntityStore().getStore(); + Holder[] holders = entityChunkComponent.takeEntityHolders(); + if (holders == null) { + return; + } + + int holderCount = holders.length; + for (int i = holderCount - 1; i >= 0; --i) { + Holder holder = holders[i]; + Archetype archetype = holder.getArchetype(); + if (archetype == null) { + holders[i] = holders[--holderCount]; + holders[holderCount] = holder; + continue; + } + + if (archetype.isEmpty()) { + refixes$LOGGER.atSevere().log("Empty archetype entity holder: %s (#%d)", holder, i); + holders[i] = holders[--holderCount]; + holders[holderCount] = holder; + worldChunkComponent.markNeedsSaving(); + continue; + } + + if (archetype.count() == 1 && archetype.contains(Nameplate.getComponentType())) { + refixes$LOGGER.atSevere().log("Nameplate only entity holder: %s (#%d)", holder, i); + holders[i] = holders[--holderCount]; + holders[holderCount] = holder; + worldChunkComponent.markNeedsSaving(); + continue; + } + + TransformComponent transformComponent = + (TransformComponent) holder.getComponent(TransformComponent.getComponentType()); + if (transformComponent == null) { + refixes$LOGGER.atWarning().log( + "EntityChunkLoadingSystem#onComponentRemoved(): skipping entity holder with null TransformComponent (chunk ref: %s)", + ref); + holders[i] = holders[--holderCount]; + holders[holderCount] = holder; + worldChunkComponent.markNeedsSaving(); + continue; + } + + transformComponent.setChunkLocation(ref, worldChunkComponent); + } + + Ref[] refs = entityStore.addEntities(holders, 0, holderCount, AddReason.LOAD); + for (int i = 0; i < refs.length && refs[i].isValid(); ++i) { + entityChunkComponent.loadEntityReference(refs[i]); + } + } +} diff --git a/early/src/main/java/cc/irori/refixes/early/mixin/MixinMarkerAddRemoveSystem.java b/early/src/main/java/cc/irori/refixes/early/mixin/MixinMarkerAddRemoveSystem.java index 4adfacd..f23f2dd 100644 --- a/early/src/main/java/cc/irori/refixes/early/mixin/MixinMarkerAddRemoveSystem.java +++ b/early/src/main/java/cc/irori/refixes/early/mixin/MixinMarkerAddRemoveSystem.java @@ -1,6 +1,7 @@ package cc.irori.refixes.early.mixin; import cc.irori.refixes.early.util.Logs; +import com.hypixel.hytale.component.AddReason; import com.hypixel.hytale.component.CommandBuffer; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.RemoveReason; @@ -11,6 +12,7 @@ import com.hypixel.hytale.server.npc.systems.SpawnReferenceSystems; import com.hypixel.hytale.server.spawning.spawnmarkers.SpawnMarkerEntity; import com.llamalad7.mixinextras.sugar.Local; +import java.util.UUID; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; @@ -28,9 +30,16 @@ public abstract class MixinMarkerAddRemoveSystem { @Unique private static final ThreadLocal refixes$NPC_REFERENCES = new ThreadLocal<>(); + @Unique + private static final ThreadLocal refixes$REMOVED_UUID = new ThreadLocal<>(); + @Unique private static final ThreadLocal refixes$WRAPPING = ThreadLocal.withInitial(() -> false); + @Shadow + public abstract void onEntityAdded( + Ref ref, AddReason reason, Store store, CommandBuffer commandBuffer); + @Shadow public abstract void onEntityRemove( Ref ref, @@ -38,6 +47,31 @@ public abstract void onEntityRemove( Store store, CommandBuffer commandBuffer); + @Inject(method = "onEntityAdded", at = @At("HEAD"), cancellable = true) + private void refixes$wrapOnEntityAdded( + Ref ref, + AddReason reason, + Store store, + CommandBuffer commandBuffer, + CallbackInfo ci) { + if (reason != AddReason.LOAD) { + return; + } + if (refixes$WRAPPING.get()) { + return; + } + ci.cancel(); + refixes$WRAPPING.set(true); + try { + onEntityAdded(ref, reason, store, commandBuffer); + } catch (Exception e) { + refixes$LOGGER.atWarning().withCause(e).log( + "MarkerAddRemoveSystem#onEntityAdded(): Failed to process spawn marker on load, discarding"); + } finally { + refixes$WRAPPING.set(false); + } + } + @Inject(method = "onEntityRemove", at = @At("HEAD"), cancellable = true) private void refixes$wrapOnEntityRemove( Ref ref, @@ -52,14 +86,15 @@ public abstract void onEntityRemove( refixes$WRAPPING.set(true); try { onEntityRemove(ref, reason, store, commandBuffer); - } catch (ArrayIndexOutOfBoundsException e) { + } catch (Exception e) { refixes$LOGGER.atWarning().withCause(e).log( - "MarkerAddRemoveSystem#onEntityRemove(): Array index out of bounds while removing NPC references"); + "MarkerAddRemoveSystem#onEntityRemove(): Unhandled exception while removing NPC references"); } finally { refixes$WRAPPING.set(false); } } + // Redirects getNpcReferences() to fix the AIOOBE crash @Redirect( method = "onEntityRemove", at = @@ -67,10 +102,68 @@ public abstract void onEntityRemove( value = "INVOKE", target = "Lcom/hypixel/hytale/server/spawning/spawnmarkers/SpawnMarkerEntity;getNpcReferences()[Lcom/hypixel/hytale/server/core/entity/reference/InvalidatablePersistentRef;")) - private InvalidatablePersistentRef[] refixes$storeNpcReferences(SpawnMarkerEntity instance) { + private InvalidatablePersistentRef[] refixes$storeAndFilterNpcReferences(SpawnMarkerEntity instance) { InvalidatablePersistentRef[] refs = instance.getNpcReferences(); refixes$NPC_REFERENCES.set(refs); - return refs; + + if (refs == null) { + return null; + } + + UUID removedUuid = refixes$REMOVED_UUID.get(); + if (removedUuid == null) { + return refs; + } + + // find and remove the entry matching the removed entity's UUID. + int matchIndex = -1; + for (int i = 0; i < refs.length; i++) { + if (refs[i] != null && refs[i].getUuid().equals(removedUuid)) { + matchIndex = i; + break; + } + } + + if (matchIndex == -1) { + // return a copy with one fewer element to match the allocation size. + refixes$LOGGER.atWarning().log( + "MarkerAddRemoveSystem#onEntityRemove(): UUID %s not found in npcReferences (length=%d) for marker %s, " + + "returning truncated array to prevent AIOOBE", + removedUuid, refs.length, instance.getSpawnMarkerId()); + if (refs.length <= 1) { + return new InvalidatablePersistentRef[0]; + } + InvalidatablePersistentRef[] truncated = new InvalidatablePersistentRef[refs.length - 1]; + System.arraycopy(refs, 0, truncated, 0, truncated.length); + return truncated; + } + + // if uuid found, copy every element except the one with the matching uuid + InvalidatablePersistentRef[] filtered = new InvalidatablePersistentRef[refs.length - 1]; + System.arraycopy(refs, 0, filtered, 0, matchIndex); + System.arraycopy(refs, matchIndex + 1, filtered, matchIndex, refs.length - matchIndex - 1); + return filtered; + } + + /** + * Captures the UUID of the entity being removed into a ThreadLocal, + * to make it available in refixes$storeAndFilterNpcReferences + */ + @Inject( + method = "onEntityRemove", + at = + @At( + value = "INVOKE", + target = + "Lcom/hypixel/hytale/server/spawning/spawnmarkers/SpawnMarkerEntity;getNpcReferences()[Lcom/hypixel/hytale/server/core/entity/reference/InvalidatablePersistentRef;")) + private void refixes$captureRemovedUuid( + Ref ref, + RemoveReason reason, + Store store, + CommandBuffer commandBuffer, + CallbackInfo ci, + @Local(name = "uuid") UUID uuid) { + refixes$REMOVED_UUID.set(uuid); } @Inject( @@ -91,6 +184,7 @@ public abstract void onEntityRemove( @Local(name = "spawnMarkerComponent") SpawnMarkerEntity spawnMarkerComponent) { InvalidatablePersistentRef[] refs = refixes$NPC_REFERENCES.get(); refixes$NPC_REFERENCES.remove(); + refixes$REMOVED_UUID.remove(); if (refs == null) { refixes$LOGGER.atWarning().log( diff --git a/early/src/main/java/cc/irori/refixes/early/mixin/MixinMotionControllerBase.java b/early/src/main/java/cc/irori/refixes/early/mixin/MixinMotionControllerBase.java new file mode 100644 index 0000000..e25d2b7 --- /dev/null +++ b/early/src/main/java/cc/irori/refixes/early/mixin/MixinMotionControllerBase.java @@ -0,0 +1,29 @@ +package cc.irori.refixes.early.mixin; + +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.server.npc.movement.controllers.MotionControllerBase; +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.CallbackInfoReturnable; + +@Mixin(MotionControllerBase.class) +public class MixinMotionControllerBase { + + @Shadow + protected Vector3d translation; + + @Inject( + method = "steer0", + at = + @At( + value = "INVOKE", + target = + "Lcom/hypixel/hytale/server/npc/movement/controllers/MotionControllerBase;executeMove(Lcom/hypixel/hytale/component/Ref;Lcom/hypixel/hytale/server/npc/role/Role;DLcom/hypixel/hytale/math/vector/Vector3d;Lcom/hypixel/hytale/component/ComponentAccessor;)D")) + private void refixes$guardNaNTranslation(CallbackInfoReturnable cir) { + if (!Double.isFinite(translation.x) || !Double.isFinite(translation.y) || !Double.isFinite(translation.z)) { + translation.assign(0.0); + } + } +} diff --git a/early/src/main/java/cc/irori/refixes/early/mixin/MixinPlayer.java b/early/src/main/java/cc/irori/refixes/early/mixin/MixinPlayer.java new file mode 100644 index 0000000..369ee29 --- /dev/null +++ b/early/src/main/java/cc/irori/refixes/early/mixin/MixinPlayer.java @@ -0,0 +1,42 @@ +package cc.irori.refixes.early.mixin; + +import cc.irori.refixes.early.util.Logs; +import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.server.core.entity.Entity; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.universe.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +// Fixes PlayerReadyEvent being dispatched on the Scheduler thread instead of the World thread. + +@Mixin(Player.class) +public abstract class MixinPlayer { + + @Shadow + public abstract void handleClientReady(boolean forced); + + @Unique + private static final HytaleLogger refixes$LOGGER = Logs.logger(); + + @Inject(method = "handleClientReady", at = @At("HEAD"), cancellable = true) + private void refixes$redirectToWorldThread(boolean forced, CallbackInfo ci) { + World world = ((Entity) (Object) this).getWorld(); + if (world == null) { + return; + } + if (!world.isInThread()) { + ci.cancel(); + if (world.isAlive()) { + world.execute(() -> handleClientReady(forced)); + } else { + refixes$LOGGER.atWarning().log( + "Player#handleClientReady(): World %s is not alive, discarding event", world.getName()); + } + } + } +} diff --git a/early/src/main/java/cc/irori/refixes/early/mixin/MixinPortalDeviceSummonPage.java b/early/src/main/java/cc/irori/refixes/early/mixin/MixinPortalDeviceSummonPage.java index 877f615..2c3c1a3 100644 --- a/early/src/main/java/cc/irori/refixes/early/mixin/MixinPortalDeviceSummonPage.java +++ b/early/src/main/java/cc/irori/refixes/early/mixin/MixinPortalDeviceSummonPage.java @@ -1,9 +1,11 @@ package cc.irori.refixes.early.mixin; +import cc.irori.refixes.early.util.Logs; import cc.irori.refixes.early.util.SharedInstanceConstants; import com.hypixel.hytale.builtin.instances.removal.InstanceDataResource; import com.hypixel.hytale.builtin.portals.resources.PortalWorld; import com.hypixel.hytale.builtin.portals.ui.PortalDeviceSummonPage; +import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.math.vector.Transform; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.WorldConfig; @@ -12,6 +14,7 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @@ -19,6 +22,9 @@ @Mixin(PortalDeviceSummonPage.class) public class MixinPortalDeviceSummonPage { + @Unique + private static final HytaleLogger refixes$LOGGER = Logs.logger(); + @Inject(method = "spawnReturnPortal", at = @At("HEAD"), cancellable = true) private static void refixes$preventDuplicateReturnPortal( World world, @@ -63,4 +69,23 @@ public class MixinPortalDeviceSummonPage { } } } + + /** + * Guards against PortalSpawnFinder.computeSpawnTransform() returning null, + * which causes NPE in spawnReturnPortal when calling spawnTransform.getPosition(). + */ + @Inject(method = "getSpawnTransform", at = @At("RETURN"), cancellable = true) + private static void refixes$nullGuardSpawnTransform( + World world, UUID sampleUuid, CallbackInfoReturnable> cir) { + CompletableFuture future = cir.getReturnValue(); + cir.setReturnValue(future.thenApply(transform -> { + if (transform == null) { + refixes$LOGGER.atWarning().log( + "PortalDeviceSummonPage#getSpawnTransform(): null for world %s, using fallback spawn", + world.getName()); + return new Transform(0.0, 128.0, 0.0); + } + return transform; + })); + } } diff --git a/early/src/main/java/cc/irori/refixes/early/mixin/MixinStore.java b/early/src/main/java/cc/irori/refixes/early/mixin/MixinStore.java index 5099ae2..9a7e256 100644 --- a/early/src/main/java/cc/irori/refixes/early/mixin/MixinStore.java +++ b/early/src/main/java/cc/irori/refixes/early/mixin/MixinStore.java @@ -10,7 +10,10 @@ import com.hypixel.hytale.logger.HytaleLogger; import java.lang.reflect.Constructor; import java.util.Deque; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ForkJoinWorkerThread; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import javax.annotation.Nonnull; import org.checkerframework.checker.nullness.compatqual.NonNullDecl; import org.spongepowered.asm.mixin.Final; @@ -20,6 +23,7 @@ import org.spongepowered.asm.mixin.Unique; 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.callback.CallbackInfo; /** @@ -124,4 +128,27 @@ public > void tryRemoveComponent( refixes$SUPPRESS_WRITE_ASSERT.set(false); } } + + // Redirects the CompletableFuture.join() call in shutdown0() to use a timeout + @Redirect( + method = "shutdown0", + at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;join()Ljava/lang/Object;")) + private Object refixes$joinWithTimeout(CompletableFuture future) { + boolean wasInterrupted = Thread.interrupted(); // clear interrupt flag + try { + return future.get(10, TimeUnit.SECONDS); + } catch (TimeoutException e) { + refixes$LOGGER.atWarning().log( + "Store#shutdown0(): saveAllResources timed out after 10s, continuing shutdown"); + return null; + } catch (Exception e) { + refixes$LOGGER.atWarning().withCause(e).log( + "Store#shutdown0(): saveAllResources failed, continuing shutdown"); + return null; + } finally { + if (wasInterrupted) { + Thread.currentThread().interrupt(); + } + } + } } diff --git a/early/src/main/java/cc/irori/refixes/early/mixin/MixinTurnOffTeleportersSystem.java b/early/src/main/java/cc/irori/refixes/early/mixin/MixinTurnOffTeleportersSystem.java new file mode 100644 index 0000000..8b6fa99 --- /dev/null +++ b/early/src/main/java/cc/irori/refixes/early/mixin/MixinTurnOffTeleportersSystem.java @@ -0,0 +1,67 @@ +package cc.irori.refixes.early.mixin; + +import cc.irori.refixes.early.util.Logs; +import com.hypixel.hytale.builtin.adventure.teleporter.system.TurnOffTeleportersSystem; +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.RemoveReason; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore; +import javax.annotation.Nonnull; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Unique; + +/** + * Defers TurnOffTeleportersSystem.updatePortalBlocksInWorld() calls to world.execute() + * instead of running inline during onEntityAdded/onEntityRemove callbacks. + */ +@Mixin(TurnOffTeleportersSystem.class) +public class MixinTurnOffTeleportersSystem { + + @Unique + private static final HytaleLogger refixes$LOGGER = Logs.logger(); + + @Overwrite + public void onEntityAdded( + @Nonnull Ref ref, + @Nonnull AddReason reason, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer) { + if (reason == AddReason.LOAD) { + World world = store.getExternalData().getWorld(); + world.execute(() -> { + try { + TurnOffTeleportersSystem.updatePortalBlocksInWorld(world); + } catch (Exception e) { + refixes$LOGGER.atWarning().withCause(e).log( + "TurnOffTeleportersSystem#onEntityAdded(): Failed to update portal blocks in %s", + world.getName()); + } + }); + } + } + + @Overwrite + public void onEntityRemove( + @Nonnull Ref ref, + @Nonnull RemoveReason reason, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer) { + if (reason == RemoveReason.REMOVE) { + World world = store.getExternalData().getWorld(); + world.execute(() -> { + try { + TurnOffTeleportersSystem.updatePortalBlocksInWorld(world); + } catch (Exception e) { + refixes$LOGGER.atWarning().withCause(e).log( + "TurnOffTeleportersSystem#onEntityRemove(): Failed to update portal blocks in %s", + world.getName()); + } + }); + } + } +} diff --git a/early/src/main/java/cc/irori/refixes/early/mixin/MixinUniverse.java b/early/src/main/java/cc/irori/refixes/early/mixin/MixinUniverse.java index 0e52e00..c843af8 100644 --- a/early/src/main/java/cc/irori/refixes/early/mixin/MixinUniverse.java +++ b/early/src/main/java/cc/irori/refixes/early/mixin/MixinUniverse.java @@ -1,6 +1,7 @@ package cc.irori.refixes.early.mixin; import cc.irori.refixes.early.util.Logs; +import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.Universe; @@ -23,10 +24,16 @@ public abstract class MixinUniverse { @Shadow protected abstract void lambda$removePlayer$2(PlayerRef par1, Void par2, Throwable par3); + @Inject(method = "lambda$removePlayer$0", at = @At("HEAD"), cancellable = true) + private static void refixes$guardAsyncRemoval(Ref ref, CallbackInfo ci) { + if (!ref.isValid()) { + ci.cancel(); + } + } + @Inject(method = "lambda$removePlayer$2", at = @At("HEAD"), cancellable = true) private void refixes$wrapRemovePlayerComplete(PlayerRef playerRef, Void result, Throwable error, CallbackInfo ci) { if (refixes$WRAPPING.get()) { - // Run the original method return; } diff --git a/early/src/main/java/cc/irori/refixes/early/mixin/MixinWorld.java b/early/src/main/java/cc/irori/refixes/early/mixin/MixinWorld.java index 35e8a0f..b65da1c 100644 --- a/early/src/main/java/cc/irori/refixes/early/mixin/MixinWorld.java +++ b/early/src/main/java/cc/irori/refixes/early/mixin/MixinWorld.java @@ -8,6 +8,8 @@ import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; @@ -75,4 +77,25 @@ public class MixinWorld { } return false; } + + // Redirects the config save .join() in onShutdown() to use a timeout + @Redirect( + method = "onShutdown", + at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;join()Ljava/lang/Object;")) + private Object refixes$configSaveJoinWithTimeout(CompletableFuture future) { + boolean wasInterrupted = Thread.interrupted(); + try { + return future.get(10, TimeUnit.SECONDS); + } catch (TimeoutException e) { + refixes$LOGGER.atWarning().log("World#onShutdown(): Config save timed out after 10s, continuing shutdown"); + return null; + } catch (Exception e) { + refixes$LOGGER.atWarning().withCause(e).log("World#onShutdown(): Config save failed, continuing shutdown"); + return null; + } finally { + if (wasInterrupted) { + Thread.currentThread().interrupt(); + } + } + } } diff --git a/early/src/main/resources/refixes.mixins.json b/early/src/main/resources/refixes.mixins.json index 20a0d92..3749ef7 100644 --- a/early/src/main/resources/refixes.mixins.json +++ b/early/src/main/resources/refixes.mixins.json @@ -42,6 +42,12 @@ "MixinWorld", "MixinWorldConfig", "MixinWorldMapTracker", - "MixinWorldSpawningSystem" + "MixinWorldSpawningSystem", + "MixinBlockSectionSafety", + "MixinEntityChunkLoadingSystem", + "MixinMotionControllerBase", + "MixinPlayer", + "MixinStateSupport", + "MixinTurnOffTeleportersSystem" ] } diff --git a/plugin/src/main/java/cc/irori/refixes/Refixes.java b/plugin/src/main/java/cc/irori/refixes/Refixes.java index 30a39fc..9a3bb53 100644 --- a/plugin/src/main/java/cc/irori/refixes/Refixes.java +++ b/plugin/src/main/java/cc/irori/refixes/Refixes.java @@ -161,6 +161,9 @@ private void registerEarlyOptions() { EarlyOptions.PARALLEL_ENTITY_TICKING.setSupplier( () -> experimentalConfig.getValue(ExperimentalConfig.PARALLEL_ENTITY_TICKING)); + EarlyOptions.CORRUPT_SECTION_PROTECTION.setSupplier( + () -> experimentalConfig.getValue(ExperimentalConfig.CORRUPT_SECTION_PROTECTION)); + EarlyOptions.setAvailable(true); /* Tick Sleep Optimization */ diff --git a/plugin/src/main/java/cc/irori/refixes/config/impl/ExperimentalConfig.java b/plugin/src/main/java/cc/irori/refixes/config/impl/ExperimentalConfig.java index c494bb7..153577f 100644 --- a/plugin/src/main/java/cc/irori/refixes/config/impl/ExperimentalConfig.java +++ b/plugin/src/main/java/cc/irori/refixes/config/impl/ExperimentalConfig.java @@ -9,10 +9,13 @@ public class ExperimentalConfig extends Configuration { public static final ConfigurationKey PARALLEL_ENTITY_TICKING = new ConfigurationKey<>("ParallelEntityTicking", ConfigField.BOOLEAN, false); + public static final ConfigurationKey CORRUPT_SECTION_PROTECTION = + new ConfigurationKey<>("CorruptSectionProtection", ConfigField.BOOLEAN, true); + private static final ExperimentalConfig INSTANCE = new ExperimentalConfig(); public ExperimentalConfig() { - register(PARALLEL_ENTITY_TICKING); + register(PARALLEL_ENTITY_TICKING, CORRUPT_SECTION_PROTECTION); } public static ExperimentalConfig get() { From 6236f0d9dd0608169e964264b139f0e129c9855e Mon Sep 17 00:00:00 2001 From: Xytronix <32957125+Xytronix@users.noreply.github.com> Date: Thu, 26 Feb 2026 07:07:29 +0100 Subject: [PATCH 5/9] performance improvements --- .../service/AiTickThrottlerService.java | 79 ++++++++++++++----- 1 file changed, 61 insertions(+), 18 deletions(-) diff --git a/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java b/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java index b0eee40..e480db9 100644 --- a/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java +++ b/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java @@ -102,20 +102,15 @@ private void unfreezeAll(World world, WorldState state) { } Store store = world.getEntityStore().getStore(); store.forEachEntityParallel(npcQuery, (index, archetypeChunk, commandBuffer) -> { - UUIDComponent uuid = archetypeChunk.getComponent(index, uuidType); - if (uuid == null) { + TickThrottled tickThrottled = archetypeChunk.getComponent(index, tickThrottledType); + if (tickThrottled == null) { return; } - AiLodEntry entry = state.entries.get(uuid.getUuid()); - TickThrottled tickThrottled = archetypeChunk.getComponent(index, tickThrottledType); - - if (entry != null && tickThrottled != null) { - Ref ref = archetypeChunk.getReferenceTo(index); - commandBuffer.tryRemoveComponent(ref, frozenType); - commandBuffer.tryRemoveComponent(ref, stepType); - commandBuffer.tryRemoveComponent(ref, tickThrottledType); - } + Ref ref = archetypeChunk.getReferenceTo(index); + commandBuffer.tryRemoveComponent(ref, frozenType); + commandBuffer.tryRemoveComponent(ref, stepType); + commandBuffer.tryRemoveComponent(ref, tickThrottledType); }); } @@ -140,6 +135,17 @@ private void processWorld(World world, AiTickThrottlerConfig cfg) { // Precompute player chunk positions List playerChunks = collectPlayerChunkPositions(world.getPlayerRefs()); + + // No players online: freeze all NPCs once, then skip subsequent cycles + if (playerChunks.isEmpty()) { + if (!state.frozenWithoutPlayers) { + freezeAllNpcs(store); + state.frozenWithoutPlayers = true; + } + return; + } + state.frozenWithoutPlayers = false; + long now = System.nanoTime(); int nearChunks = Math.max(0, cfg.getValue(AiTickThrottlerConfig.NEAR_CHUNKS)); @@ -150,8 +156,18 @@ private void processWorld(World world, AiTickThrottlerConfig cfg) { int maxUnfreezes = Math.max(1, cfg.getValue(AiTickThrottlerConfig.MAX_UNFREEZES_PER_TICK)); AtomicInteger unfreezeCount = new AtomicInteger(0); - // Track seen entity UUIDs to prune stale entries after iteration - Set seen = ConcurrentHashMap.newKeySet(); + float minTick = cfg.getValue(AiTickThrottlerConfig.MIN_TICK_SECONDS); + + // Pre-compute StepComponent instances for each tier to avoid per-entity allocation + float midSec = Math.max(minTick, cfg.getValue(AiTickThrottlerConfig.MID_TICK_SECONDS)); + float farSec = Math.max(minTick, cfg.getValue(AiTickThrottlerConfig.FAR_TICK_SECONDS)); + float veryFarSec = Math.max(minTick, cfg.getValue(AiTickThrottlerConfig.VERY_FAR_TICK_SECONDS)); + StepComponent midStep = new StepComponent(midSec); + StepComponent farStep = new StepComponent(farSec); + StepComponent veryFarStep = new StepComponent(veryFarSec); + + // Reuse seen set to avoid allocating a new ConcurrentHashMap each cycle + state.seen.clear(); store.forEachEntityParallel(npcQuery, (index, archetypeChunk, commandBuffer) -> { // Skip player entities @@ -170,7 +186,7 @@ private void processWorld(World world, AiTickThrottlerConfig cfg) { int entityChunkZ = ChunkUtil.chunkCoordinate(transform.getPosition().getZ()); int chunkDist = closestPlayerChunkDistance(entityChunkX, entityChunkZ, playerChunks); UUID entityId = uuid.getUuid(); - seen.add(entityId); + state.seen.add(entityId); Ref ref = archetypeChunk.getReferenceTo(index); @@ -209,7 +225,6 @@ private void processWorld(World world, AiTickThrottlerConfig cfg) { commandBuffer.ensureComponent(ref, tickThrottledType); } - float minTick = cfg.getValue(AiTickThrottlerConfig.MIN_TICK_SECONDS); long intervalNanos = (long) (Math.max(minTick, intervalSec) * 1_000_000_000.0); if (entry.intervalNanos != intervalNanos) { entry.intervalNanos = intervalNanos; @@ -217,14 +232,40 @@ private void processWorld(World world, AiTickThrottlerConfig cfg) { } if (now >= entry.nextTickNanos) { - commandBuffer.putComponent( - ref, stepType, new StepComponent((float) ((double) intervalNanos / 1_000_000_000.0))); + // Use pre-computed step for the matching tier + StepComponent step; + if (chunkDist <= midChunks) { + step = midStep; + } else if (chunkDist <= farChunks) { + step = farStep; + } else { + step = veryFarStep; + } + commandBuffer.putComponent(ref, stepType, step); entry.nextTickNanos = now + intervalNanos; } }); // Prune entries for entities no longer in the world - state.entries.keySet().retainAll(seen); + state.entries.keySet().retainAll(state.seen); + } + + private void freezeAllNpcs(Store store) { + store.forEachEntityParallel(npcQuery, (index, archetypeChunk, commandBuffer) -> { + if (playerType != null && archetypeChunk.getArchetype().contains(playerType)) { + return; + } + boolean frozen = archetypeChunk.getComponent(index, frozenType) != null; + boolean throttled = archetypeChunk.getComponent(index, tickThrottledType) != null; + if (frozen && !throttled) { + return; + } + if (!frozen) { + Ref ref = archetypeChunk.getReferenceTo(index); + commandBuffer.ensureComponent(ref, frozenType); + commandBuffer.ensureComponent(ref, tickThrottledType); + } + }); } private static double computeInterval( @@ -306,6 +347,8 @@ private boolean resolveComponentTypes() { private static final class WorldState { final Map entries = new ConcurrentHashMap<>(); + final Set seen = ConcurrentHashMap.newKeySet(); + boolean frozenWithoutPlayers; } private static final class AiLodEntry { From b8e8c7fe536b2e955728dab3b7dd420835ef5910 Mon Sep 17 00:00:00 2001 From: Xytronix <32957125+Xytronix@users.noreply.github.com> Date: Thu, 26 Feb 2026 07:41:41 +0100 Subject: [PATCH 6/9] bumped defaults --- .../refixes/config/impl/AiTickThrottlerConfig.java | 12 ++++++------ .../refixes/service/AiTickThrottlerService.java | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugin/src/main/java/cc/irori/refixes/config/impl/AiTickThrottlerConfig.java b/plugin/src/main/java/cc/irori/refixes/config/impl/AiTickThrottlerConfig.java index 6dcd0c0..233f82d 100644 --- a/plugin/src/main/java/cc/irori/refixes/config/impl/AiTickThrottlerConfig.java +++ b/plugin/src/main/java/cc/irori/refixes/config/impl/AiTickThrottlerConfig.java @@ -13,15 +13,15 @@ public class AiTickThrottlerConfig extends Configuration public static final ConfigurationKey UPDATE_INTERVAL_MS = new ConfigurationKey<>("UpdateIntervalMs", ConfigField.INTEGER, 150); - // NPCs within this chunk distance get full tick rate + // NPCs within this chunk distance get full tick rate (~48 blocks) public static final ConfigurationKey NEAR_CHUNKS = - new ConfigurationKey<>("NearChunks", ConfigField.INTEGER, 1); - // NPCs within this chunk distance get mid tick rate + new ConfigurationKey<>("NearChunks", ConfigField.INTEGER, 3); + // NPCs within this chunk distance get mid tick rate (~80 blocks) public static final ConfigurationKey MID_CHUNKS = - new ConfigurationKey<>("MidChunks", ConfigField.INTEGER, 2); - // NPCs within this chunk distance get far tick rate + new ConfigurationKey<>("MidChunks", ConfigField.INTEGER, 5); + // NPCs within this chunk distance get far tick rate (~128 blocks) public static final ConfigurationKey FAR_CHUNKS = - new ConfigurationKey<>("FarChunks", ConfigField.INTEGER, 4); + new ConfigurationKey<>("FarChunks", ConfigField.INTEGER, 8); public static final ConfigurationKey MID_TICK_SECONDS = new ConfigurationKey<>("MidTickSeconds", ConfigField.FLOAT, 0.2f); diff --git a/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java b/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java index e480db9..6456f30 100644 --- a/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java +++ b/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java @@ -36,10 +36,10 @@ * Distance-based LOD for AI entity ticking * * Freezes distant NPCs and reduces their tick rate based on chunk proximity to the nearest player - * ≤ 1 chunk — full AI tick rate - * ≤ 2 chunks — mid tick rate (0.2s) - * ≤ 4 chunks — far tick rate (0.5s) - * > 4 chunks — very far tick rate (1.0s) + * ≤ 3 chunks (~48 blocks) — full AI tick rate + * ≤ 5 chunks (~80 blocks) — mid tick rate (0.2s) + * ≤ 8 chunks (~128 blocks) — far tick rate (0.5s) + * > 8 chunks — very far tick rate (1.0s) */ public class AiTickThrottlerService { From b8de1fe8f50a9a0c1557a9179edddab099ae4c46 Mon Sep 17 00:00:00 2001 From: Xytronix <32957125+Xytronix@users.noreply.github.com> Date: Thu, 26 Feb 2026 07:47:22 +0100 Subject: [PATCH 7/9] change defaults --- .../refixes/config/impl/AiTickThrottlerConfig.java | 12 ++++++------ .../refixes/service/AiTickThrottlerService.java | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugin/src/main/java/cc/irori/refixes/config/impl/AiTickThrottlerConfig.java b/plugin/src/main/java/cc/irori/refixes/config/impl/AiTickThrottlerConfig.java index 233f82d..eea5409 100644 --- a/plugin/src/main/java/cc/irori/refixes/config/impl/AiTickThrottlerConfig.java +++ b/plugin/src/main/java/cc/irori/refixes/config/impl/AiTickThrottlerConfig.java @@ -13,15 +13,15 @@ public class AiTickThrottlerConfig extends Configuration public static final ConfigurationKey UPDATE_INTERVAL_MS = new ConfigurationKey<>("UpdateIntervalMs", ConfigField.INTEGER, 150); - // NPCs within this chunk distance get full tick rate (~48 blocks) + // NPCs within this chunk distance get full tick rate (~64 blocks) public static final ConfigurationKey NEAR_CHUNKS = - new ConfigurationKey<>("NearChunks", ConfigField.INTEGER, 3); - // NPCs within this chunk distance get mid tick rate (~80 blocks) + new ConfigurationKey<>("NearChunks", ConfigField.INTEGER, 2); + // NPCs within this chunk distance get mid tick rate (~128 blocks) public static final ConfigurationKey MID_CHUNKS = - new ConfigurationKey<>("MidChunks", ConfigField.INTEGER, 5); - // NPCs within this chunk distance get far tick rate (~128 blocks) + new ConfigurationKey<>("MidChunks", ConfigField.INTEGER, 4); + // NPCs within this chunk distance get far tick rate (~192 blocks) public static final ConfigurationKey FAR_CHUNKS = - new ConfigurationKey<>("FarChunks", ConfigField.INTEGER, 8); + new ConfigurationKey<>("FarChunks", ConfigField.INTEGER, 6); public static final ConfigurationKey MID_TICK_SECONDS = new ConfigurationKey<>("MidTickSeconds", ConfigField.FLOAT, 0.2f); diff --git a/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java b/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java index 6456f30..9172a4e 100644 --- a/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java +++ b/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java @@ -36,10 +36,10 @@ * Distance-based LOD for AI entity ticking * * Freezes distant NPCs and reduces their tick rate based on chunk proximity to the nearest player - * ≤ 3 chunks (~48 blocks) — full AI tick rate - * ≤ 5 chunks (~80 blocks) — mid tick rate (0.2s) - * ≤ 8 chunks (~128 blocks) — far tick rate (0.5s) - * > 8 chunks — very far tick rate (1.0s) + * ≤ 2 chunks (~64 blocks) — full AI tick rate + * ≤ 4 chunks (~128 blocks) — mid tick rate (0.2s) + * ≤ 6 chunks (~192 blocks) — far tick rate (0.5s) + * > 6 chunks — very far tick rate (1.0s) */ public class AiTickThrottlerService { From 423cae14497071ac298ff4a5e88e2526d559d879 Mon Sep 17 00:00:00 2001 From: Xytronix <32957125+Xytronix@users.noreply.github.com> Date: Thu, 26 Feb 2026 07:54:11 +0100 Subject: [PATCH 8/9] changed hysteresis to 0 and added maxfreezes --- .../irori/refixes/config/impl/AiTickThrottlerConfig.java | 7 +++++-- .../cc/irori/refixes/service/AiTickThrottlerService.java | 7 ++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/plugin/src/main/java/cc/irori/refixes/config/impl/AiTickThrottlerConfig.java b/plugin/src/main/java/cc/irori/refixes/config/impl/AiTickThrottlerConfig.java index eea5409..a24e152 100644 --- a/plugin/src/main/java/cc/irori/refixes/config/impl/AiTickThrottlerConfig.java +++ b/plugin/src/main/java/cc/irori/refixes/config/impl/AiTickThrottlerConfig.java @@ -36,9 +36,11 @@ public class AiTickThrottlerConfig extends Configuration new ConfigurationKey<>("LegacyCleanup", ConfigField.BOOLEAN, false); public static final ConfigurationKey ACTIVATION_HYSTERESIS_CHUNKS = - new ConfigurationKey<>("ActivationHysteresisChunks", ConfigField.INTEGER, 1); + new ConfigurationKey<>("ActivationHysteresisChunks", ConfigField.INTEGER, 0); public static final ConfigurationKey MAX_UNFREEZES_PER_TICK = new ConfigurationKey<>("MaxUnfreezesPerTick", ConfigField.INTEGER, 10); + public static final ConfigurationKey MAX_FREEZES_PER_TICK = + new ConfigurationKey<>("MaxFreezesPerTick", ConfigField.INTEGER, 20); private static final AiTickThrottlerConfig INSTANCE = new AiTickThrottlerConfig(); @@ -56,7 +58,8 @@ public AiTickThrottlerConfig() { MIN_TICK_SECONDS, LEGACY_CLEANUP, ACTIVATION_HYSTERESIS_CHUNKS, - MAX_UNFREEZES_PER_TICK); + MAX_UNFREEZES_PER_TICK, + MAX_FREEZES_PER_TICK); } public static AiTickThrottlerConfig get() { diff --git a/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java b/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java index 9172a4e..87fb67c 100644 --- a/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java +++ b/plugin/src/main/java/cc/irori/refixes/service/AiTickThrottlerService.java @@ -154,7 +154,9 @@ private void processWorld(World world, AiTickThrottlerConfig cfg) { int hysteresis = Math.max(0, cfg.getValue(AiTickThrottlerConfig.ACTIVATION_HYSTERESIS_CHUNKS)); int maxUnfreezes = Math.max(1, cfg.getValue(AiTickThrottlerConfig.MAX_UNFREEZES_PER_TICK)); + int maxFreezes = Math.max(1, cfg.getValue(AiTickThrottlerConfig.MAX_FREEZES_PER_TICK)); AtomicInteger unfreezeCount = new AtomicInteger(0); + AtomicInteger freezeCount = new AtomicInteger(0); float minTick = cfg.getValue(AiTickThrottlerConfig.MIN_TICK_SECONDS); @@ -219,11 +221,14 @@ private void processWorld(World world, AiTickThrottlerConfig cfg) { return; } - AiLodEntry entry = state.entries.computeIfAbsent(uuid.getUuid(), _k -> new AiLodEntry()); if (!throttled) { + if (freezeCount.incrementAndGet() > maxFreezes) { + return; + } commandBuffer.ensureComponent(ref, frozenType); commandBuffer.ensureComponent(ref, tickThrottledType); } + AiLodEntry entry = state.entries.computeIfAbsent(entityId, _k -> new AiLodEntry()); long intervalNanos = (long) (Math.max(minTick, intervalSec) * 1_000_000_000.0); if (entry.intervalNanos != intervalNanos) { From 6e24753403a1e1481a5fd30d334ff184c903e178 Mon Sep 17 00:00:00 2001 From: Dimotai Date: Sat, 28 Feb 2026 02:34:55 -0500 Subject: [PATCH 9/9] Add MixinHideEntitySystems to catch IllegalStateException during cross-world teleport --- .../early/mixin/MixinHideEntitySystems.java | 61 +++++++++++++++++++ early/src/main/resources/refixes.mixins.json | 1 + 2 files changed, 62 insertions(+) create mode 100644 early/src/main/java/cc/irori/refixes/early/mixin/MixinHideEntitySystems.java diff --git a/early/src/main/java/cc/irori/refixes/early/mixin/MixinHideEntitySystems.java b/early/src/main/java/cc/irori/refixes/early/mixin/MixinHideEntitySystems.java new file mode 100644 index 0000000..6fd04ac --- /dev/null +++ b/early/src/main/java/cc/irori/refixes/early/mixin/MixinHideEntitySystems.java @@ -0,0 +1,61 @@ +package cc.irori.refixes.early.mixin; + +import cc.irori.refixes.early.util.Logs; +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.server.core.modules.entity.system.HideEntitySystems; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +// Fixes "java.lang.IllegalStateException: Invalid entity reference!" +// in HideEntitySystems$AdventurePlayerSystem.tick() when a player teleports +// between worlds and their entity reference is invalidated mid-tick. + +@Mixin(HideEntitySystems.AdventurePlayerSystem.class) +public abstract class MixinHideEntitySystems { + + @Unique + private static final HytaleLogger refixes$LOGGER = Logs.logger(); + + @Unique + private static final ThreadLocal refixes$WRAPPING = ThreadLocal.withInitial(() -> false); + + @Shadow + public abstract void tick( + float deltaTime, + int entityIndex, + ArchetypeChunk chunk, + Store store, + CommandBuffer commandBuffer); + + @Inject(method = "tick", at = @At("HEAD"), cancellable = true) + private void refixes$wrapTick( + float deltaTime, + int entityIndex, + ArchetypeChunk chunk, + Store store, + CommandBuffer commandBuffer, + CallbackInfo ci) { + if (refixes$WRAPPING.get()) { + return; + } + + ci.cancel(); + refixes$WRAPPING.set(true); + try { + tick(deltaTime, entityIndex, chunk, store, commandBuffer); + } catch (IllegalStateException e) { + refixes$LOGGER.atWarning().log( + "HideEntitySystems$AdventurePlayerSystem.tick(): Skipping tick for invalid entity reference (likely mid-teleport), discarding"); + } finally { + refixes$WRAPPING.set(false); + } + } +} diff --git a/early/src/main/resources/refixes.mixins.json b/early/src/main/resources/refixes.mixins.json index 3749ef7..ac0dd92 100644 --- a/early/src/main/resources/refixes.mixins.json +++ b/early/src/main/resources/refixes.mixins.json @@ -17,6 +17,7 @@ "MixinFluidPlugin", "MixinFluidReplicateChanges", "MixinGamePacketHandler", + "MixinHideEntitySystems", "MixinHytaleServer", "MixinHytaleServerConfig", "MixinInstancesPlugin",