201 lines
13 KiB
Java
201 lines
13 KiB
Java
package dev.modzuozhi;
|
|
|
|
import dev.modzuozhi.core.dimthread.DimThreadCore;
|
|
import net.minecraft.core.BlockPos;
|
|
import net.minecraft.core.registries.BuiltInRegistries;
|
|
import net.minecraft.resources.ResourceLocation;
|
|
import net.neoforged.bus.api.SubscribeEvent;
|
|
import net.neoforged.fml.common.EventBusSubscriber;
|
|
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
|
import java.util.concurrent.CountDownLatch;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
/** Local opt-in fixture. Removing the CC safe phase must fail the real identity check. */
|
|
@EventBusSubscriber(modid = "dimensionalripper")
|
|
public final class OwnershipRegression {
|
|
private static boolean ran;
|
|
private static boolean verified;
|
|
private static long worldTime;
|
|
@SubscribeEvent
|
|
public static void tick(ServerTickEvent.Post event) throws Exception {
|
|
var server = event.getServer();
|
|
if (verified && server.getTickCount() >= 80) {
|
|
DimThreadCore.MANAGER.setPaused(server, true);
|
|
if (server.overworld().getGameTime() <= worldTime) throw new AssertionError("Parallel worlds did not resume");
|
|
ModZuozhi.LOGGER.info("OWNERSHIP RESUME PASS: world continued ticking after compatibility phases");
|
|
verified = false;
|
|
server.halt(false);
|
|
}
|
|
if (ran || server.getTickCount() < 20) return;
|
|
ran = true;
|
|
var level = server.overworld();
|
|
var manager = DimThreadCore.MANAGER;
|
|
var release = new CountDownLatch(1);
|
|
float originalRate = server.tickRateManager().tickrate();
|
|
try {
|
|
manager.setPaused(server, true);
|
|
level.setChunkForced(0, 0, true);
|
|
level.getChunk(0, 0);
|
|
var pos = new BlockPos(8, 110, 8);
|
|
var block = BuiltInRegistries.BLOCK.get(ResourceLocation.parse("computercraft:monitor_normal"));
|
|
level.setBlockAndUpdate(pos, block.defaultBlockState());
|
|
var monitor = level.getBlockEntity(pos);
|
|
if (monitor == null) throw new AssertionError("Monitor fixture not installed");
|
|
var scheduler = Class.forName("dan200.computercraft.shared.util.TickScheduler");
|
|
var tokenClass = Class.forName("dan200.computercraft.shared.util.TickScheduler$Token");
|
|
var token = tokenClass.getConstructor(net.minecraft.world.level.block.entity.BlockEntity.class).newInstance(monitor);
|
|
scheduler.getMethod("schedule", tokenClass).invoke(null, token);
|
|
var entered = new CountDownLatch(1);
|
|
var bridgeFailure = new java.util.concurrent.atomic.AtomicReference<Throwable>();
|
|
manager.getLoop(level).execute(() -> {
|
|
// A Sable-like main-thread dependency: safe waiting must pump this callback.
|
|
server.execute(release::countDown);
|
|
entered.countDown();
|
|
try {
|
|
if (!release.await(10, TimeUnit.SECONDS)) throw new AssertionError("Main callback was not pumped");
|
|
dev.modzuozhi.core.compat.SableMainThreadBridge.runForLevel(level, () -> {
|
|
if (!server.isSameThread() || level.getBlockEntity(pos) != monitor)
|
|
throw new AssertionError("Bridge did not obtain main-thread world ownership");
|
|
});
|
|
} catch (Throwable e) { bridgeFailure.set(e); }
|
|
});
|
|
// Amplify idle polling waste: stopping an idle poll must not wait a 1-second tick budget.
|
|
server.tickRateManager().setTickRate(1.0F);
|
|
manager.setPaused(server, false);
|
|
if (!entered.await(10, TimeUnit.SECONDS)) throw new AssertionError("Worker did not start");
|
|
if (level.getBlockEntity(pos) != null) throw new AssertionError("Did not reproduce guarded main-thread lookup");
|
|
for (int i = 0; i < 1000; i++) level.getBlockEntity(pos);
|
|
long start = System.nanoTime();
|
|
scheduler.getMethod("tick").invoke(null);
|
|
server.tickRateManager().setTickRate(originalRate);
|
|
if (System.nanoTime() - start > TimeUnit.MILLISECONDS.toNanos(750))
|
|
throw new AssertionError("Safe phase waited out idle polling budget after pause request");
|
|
if (bridgeFailure.get() != null) throw new AssertionError("Sable bridge failed", bridgeFailure.get());
|
|
ModZuozhi.LOGGER.info("CC PHASE MEASUREMENT: {}", dev.modzuozhi.core.compat.ThreadOwnershipDiagnostics.timing());
|
|
manager.setPaused(server, true);
|
|
if (level.getBlockEntity(pos) != monitor) throw new AssertionError("Monitor identity changed");
|
|
var state = tokenClass.getDeclaredField("$state");
|
|
state.setAccessible(true);
|
|
if (!state.get(token).toString().equals("IDLE")) throw new AssertionError("CC token was not completed");
|
|
// The same scope must preserve an existing pause, even when nested or throwing.
|
|
var expected = new IllegalArgumentException("regression sentinel");
|
|
try {
|
|
manager.runMainThreadPhase(server, "outer-test", () -> manager.runMainThreadPhase(server, "inner-test", () -> { throw expected; }));
|
|
throw new AssertionError("Phase swallowed exception");
|
|
} catch (IllegalArgumentException failure) {
|
|
if (failure != expected) throw failure;
|
|
}
|
|
if (!manager.getLoop(level).isPaused()) throw new AssertionError("Existing pause was lost");
|
|
manager.setPaused(server, false);
|
|
try {
|
|
manager.runMainThreadPhase(server, "exception-resume-test", () -> { throw expected; });
|
|
throw new AssertionError("Running phase swallowed exception");
|
|
} catch (IllegalArgumentException failure) {
|
|
if (failure != expected) throw failure;
|
|
}
|
|
if (manager.getLoop(level).isPaused()) throw new AssertionError("Failed phase did not resume running world");
|
|
manager.setPaused(server, true);
|
|
// Removed owners are valid stale tokens. Real identity mismatches must still throw.
|
|
var removed = net.minecraft.world.level.block.entity.BlockEntityType.CHEST.create(pos, net.minecraft.world.level.block.Blocks.CHEST.defaultBlockState());
|
|
removed.setLevel(level);
|
|
var removedToken = tokenClass.getConstructor(net.minecraft.world.level.block.entity.BlockEntity.class).newInstance(removed);
|
|
scheduler.getMethod("schedule", tokenClass).invoke(null, removedToken);
|
|
removed.setRemoved();
|
|
scheduler.getMethod("tick").invoke(null);
|
|
if (!state.get(removedToken).toString().equals("IDLE")) throw new AssertionError("Removed token not discarded");
|
|
removed.clearRemoved();
|
|
scheduler.getMethod("schedule", tokenClass).invoke(null, removedToken);
|
|
try {
|
|
scheduler.getMethod("tick").invoke(null);
|
|
throw new AssertionError("Real mismatch was hidden");
|
|
} catch (java.lang.reflect.InvocationTargetException failure) {
|
|
if (!(failure.getCause() instanceof IllegalStateException)) throw failure;
|
|
}
|
|
var far = new BlockPos(1600000, 100, 1600000);
|
|
var unloaded = net.minecraft.world.level.block.entity.BlockEntityType.CHEST.create(far, net.minecraft.world.level.block.Blocks.CHEST.defaultBlockState());
|
|
unloaded.setLevel(level);
|
|
var unloadedToken = tokenClass.getConstructor(net.minecraft.world.level.block.entity.BlockEntity.class).newInstance(unloaded);
|
|
scheduler.getMethod("schedule", tokenClass).invoke(null, unloadedToken);
|
|
int chunks = level.getChunkSource().getLoadedChunksCount();
|
|
scheduler.getMethod("tick").invoke(null);
|
|
if (!state.get(unloadedToken).toString().equals("UNLOADED") || chunks != level.getChunkSource().getLoadedChunksCount())
|
|
throw new AssertionError("Unloaded token forced chunk loading or was dropped");
|
|
scheduler.getMethod("onChunkUnload", net.minecraft.world.level.chunk.LevelChunk.class).invoke(null,
|
|
new net.minecraft.world.level.chunk.LevelChunk(level, new net.minecraft.world.level.ChunkPos(far)));
|
|
if (!state.get(unloadedToken).toString().equals("IDLE")) throw new AssertionError("Unload callback lost delayed token");
|
|
scheduler.getMethod("schedule", tokenClass).invoke(null, unloadedToken);
|
|
scheduler.getMethod("tick").invoke(null);
|
|
// Ticket promotion requeues a delayed token; unload discards it without masking errors.
|
|
scheduler.getMethod("onChunkTicketChanged", net.minecraft.server.level.ServerLevel.class, long.class, int.class, int.class)
|
|
.invoke(null, level, net.minecraft.world.level.ChunkPos.asLong(far), 40, 31);
|
|
if (!state.get(unloadedToken).toString().equals("SCHEDULED")) throw new AssertionError("Ticket callback lost token");
|
|
unloaded.setRemoved();
|
|
scheduler.getMethod("tick").invoke(null);
|
|
var report = server.fillSystemReport(new net.minecraft.SystemReport()).toLineSeparatedString();
|
|
if (!report.contains("DimensionalRipper Thread Ownership") || !report.contains("observedOwner=")
|
|
|| !report.contains("8, 110, 8") || !report.contains("rejected=1001"))
|
|
throw new AssertionError("Crash report lacks ownership evidence: " + report);
|
|
if (!report.contains("handoff=completed-owner-restored")) throw new AssertionError("Bridge ownership history missing");
|
|
concurrentCallbacks(server, scheduler, tokenClass, state);
|
|
ModZuozhi.LOGGER.info("OWNERSHIP EDGE PASS: nested/exception pauses, removed/mismatched/unloaded tokens, ticket callback, crash diagnostics");
|
|
ModZuozhi.LOGGER.info("OWNERSHIP REGRESSION PASS: real CC monitor identity, callback pumping, scheduled tick; elapsed={}ms", (System.nanoTime()-start)/1_000_000.0);
|
|
worldTime = level.getGameTime();
|
|
level.setBlockAndUpdate(pos, net.minecraft.world.level.block.Blocks.AIR.defaultBlockState());
|
|
verified = true;
|
|
} finally {
|
|
server.tickRateManager().setTickRate(originalRate);
|
|
release.countDown();
|
|
manager.setPaused(server, !verified);
|
|
if (!verified) server.halt(false);
|
|
}
|
|
}
|
|
|
|
private static void concurrentCallbacks(net.minecraft.server.MinecraftServer server, Class<?> scheduler,
|
|
Class<?> tokenClass, java.lang.reflect.Field state) throws Exception {
|
|
var levels = java.util.List.of(server.overworld(), server.getLevel(net.minecraft.world.level.Level.NETHER));
|
|
var tokens = new Object[2][40];
|
|
var owners = new net.minecraft.world.level.block.entity.BlockEntity[2][40];
|
|
for (int d = 0; d < 2; d++) for (int i = 0; i < 40; i++) {
|
|
var pos = new BlockPos(1600000 + i * 16, 100, 1600000);
|
|
var owner = net.minecraft.world.level.block.entity.BlockEntityType.CHEST.create(pos, net.minecraft.world.level.block.Blocks.CHEST.defaultBlockState());
|
|
owner.setLevel(levels.get(d));
|
|
owners[d][i] = owner;
|
|
tokens[d][i] = tokenClass.getConstructor(net.minecraft.world.level.block.entity.BlockEntity.class).newInstance(owner);
|
|
scheduler.getMethod("schedule", tokenClass).invoke(null, tokens[d][i]);
|
|
}
|
|
scheduler.getMethod("tick").invoke(null); // all 80 are deferred, no chunks are loaded
|
|
var gate = new CountDownLatch(2);
|
|
var done = new CountDownLatch(2);
|
|
var failure = new java.util.concurrent.atomic.AtomicReference<Throwable>();
|
|
for (int d = 0; d < 2; d++) {
|
|
final int dimension = d;
|
|
var level = levels.get(d);
|
|
DimThreadCore.MANAGER.getLoop(level).execute(() -> {
|
|
try {
|
|
gate.countDown();
|
|
if (!gate.await(10, TimeUnit.SECONDS)) throw new AssertionError("Dimension callback overlap not reached");
|
|
for (int i = 0; i < 40; i++) {
|
|
var pos = owners[dimension][i].getBlockPos();
|
|
if (i % 2 == 0) scheduler.getMethod("onChunkUnload", net.minecraft.world.level.chunk.LevelChunk.class)
|
|
.invoke(null, new net.minecraft.world.level.chunk.LevelChunk(level, new net.minecraft.world.level.ChunkPos(pos)));
|
|
else scheduler.getMethod("onChunkTicketChanged", net.minecraft.server.level.ServerLevel.class, long.class, int.class, int.class)
|
|
.invoke(null, level, net.minecraft.world.level.ChunkPos.asLong(pos), 40, 31);
|
|
}
|
|
} catch (Throwable error) { failure.set(error); }
|
|
finally { done.countDown(); }
|
|
});
|
|
}
|
|
DimThreadCore.MANAGER.setPaused(server, false);
|
|
server.managedBlock(() -> done.getCount() == 0);
|
|
DimThreadCore.MANAGER.setPaused(server, true);
|
|
if (failure.get() != null) throw new AssertionError("Concurrent callbacks failed", failure.get());
|
|
for (int d = 0; d < 2; d++) for (int i = 0; i < 40; i++) {
|
|
String expected = i % 2 == 0 ? "IDLE" : "SCHEDULED";
|
|
if (!state.get(tokens[d][i]).toString().equals(expected)) throw new AssertionError("Delayed token lost across dimensions");
|
|
owners[d][i].setRemoved();
|
|
}
|
|
scheduler.getMethod("tick").invoke(null);
|
|
ModZuozhi.LOGGER.info("OWNERSHIP CALLBACK PASS: 80 deferred tokens, concurrent overworld/nether ticket/unload callbacks");
|
|
}
|
|
}
|