package dev.modzuozhi.mixin; import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import dev.modzuozhi.core.dimthread.DimThreadCore; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.chunk.LevelChunk; import net.neoforged.neoforge.server.ServerLifecycleHooks; import org.spongepowered.asm.mixin.*; import java.util.Queue; /** CC remains optional. Keep its identity check intact, but run it with exclusive ownership. */ @Pseudo @Mixin(targets = "dan200.computercraft.shared.util.TickScheduler", remap = false) public abstract class ComputerCraftTickSchedulerMixin { @Shadow @Final private static Queue toTick; @Unique private static final Object dimensionalripper$schedulerLock = new Object(); @WrapMethod(method = "tick()V") private static void dimensionalripper$safeTick(Operation original) { // No tokens -> no world access, no global barrier. New tokens can wait until next tick. if (toTick.isEmpty()) return; var server = ServerLifecycleHooks.getCurrentServer(); if (server == null) throw new IllegalStateException("CC tick without a running server"); DimThreadCore.MANAGER.runMainThreadPhase(server, "CC TickScheduler", () -> { synchronized (dimensionalripper$schedulerLock) { original.call(); } }); } // CC's delayed map is global across dimensions. These callbacks only manipulate tokens; // neither loads chunks nor waits on the main thread. Never take this lock before draining. @WrapMethod(method = "onChunkTicketChanged") private static void dimensionalripper$ticket(ServerLevel level, long pos, int oldTicket, int newTicket, Operation original) { synchronized (dimensionalripper$schedulerLock) { original.call(level, pos, oldTicket, newTicket); } } @WrapMethod(method = "onChunkUnload") private static void dimensionalripper$unload(LevelChunk chunk, Operation original) { synchronized (dimensionalripper$schedulerLock) { original.call(chunk); } } }