Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
@@ -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
Original file line number Diff line number Diff line change
@@ -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<Double> cir) {
if (!Double.isFinite(translation.x) || !Double.isFinite(translation.y) || !Double.isFinite(translation.z)) {
translation.assign(0.0);
}
}
}
Loading