Skip to content
3 changes: 3 additions & 0 deletions early/src/main/java/cc/irori/refixes/early/EarlyOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public final class EarlyOptions {
public static final Value<Boolean> SHARED_INSTANCES_ENABLED = new Value<>();
public static final Value<String[]> SHARED_INSTANCES_EXCLUDED_PREFIXES = new Value<>();

/* Corrupt Section Protection */
public static final Value<Boolean> CORRUPT_SECTION_PROTECTION = new Value<>();

// Private constructor to prevent instantiation
private EarlyOptions() {}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Boolean> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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]);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Boolean> refixes$WRAPPING = ThreadLocal.withInitial(() -> false);

@Shadow
public abstract void tick(
float deltaTime,
int entityIndex,
ArchetypeChunk<EntityStore> chunk,
Store<EntityStore> store,
CommandBuffer<EntityStore> commandBuffer);

@Inject(method = "tick", at = @At("HEAD"), cancellable = true)
private void refixes$wrapTick(
float deltaTime,
int entityIndex,
ArchetypeChunk<EntityStore> chunk,
Store<EntityStore> store,
CommandBuffer<EntityStore> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -28,16 +30,48 @@ public abstract class MixinMarkerAddRemoveSystem {
@Unique
private static final ThreadLocal<InvalidatablePersistentRef[]> refixes$NPC_REFERENCES = new ThreadLocal<>();

@Unique
private static final ThreadLocal<UUID> refixes$REMOVED_UUID = new ThreadLocal<>();

@Unique
private static final ThreadLocal<Boolean> refixes$WRAPPING = ThreadLocal.withInitial(() -> false);

@Shadow
public abstract void onEntityAdded(
Ref<EntityStore> ref, AddReason reason, Store<EntityStore> store, CommandBuffer<EntityStore> commandBuffer);

@Shadow
public abstract void onEntityRemove(
Ref<EntityStore> ref,
RemoveReason reason,
Store<EntityStore> store,
CommandBuffer<EntityStore> commandBuffer);

@Inject(method = "onEntityAdded", at = @At("HEAD"), cancellable = true)
private void refixes$wrapOnEntityAdded(
Ref<EntityStore> ref,
AddReason reason,
Store<EntityStore> store,
CommandBuffer<EntityStore> 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<EntityStore> ref,
Expand All @@ -52,25 +86,84 @@ 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 =
@At(
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<EntityStore> ref,
RemoveReason reason,
Store<EntityStore> store,
CommandBuffer<EntityStore> commandBuffer,
CallbackInfo ci,
@Local(name = "uuid") UUID uuid) {
refixes$REMOVED_UUID.set(uuid);
}

@Inject(
Expand All @@ -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(
Expand Down
Loading