feat: protect mod payloads with main-thread ownership in 1.1.17

This commit is contained in:
Disy committed 2026-10-06 17:36:00 +08:00
1 parent 691e949691
commit 763c015103
29 files changed
+1552 -53

No files matched your search

@@ -2,13 +2,11 @@ package dev.modzuozhi;
import com.mojang.authlib.GameProfile;
import dev.modzuozhi.core.compat.SableAssemblyDispatch;
import dev.modzuozhi.core.compat.SableMainThreadBridge;
import dev.modzuozhi.core.compat.PayloadMainThreadDispatch;
import dev.modzuozhi.core.dimthread.DimThreadCore;
import dev.modzuozhi.core.dimthread.FineGrainScheduler;
import dev.modzuozhi.core.dimthread.FaultGuard;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ClientInformation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.material.Fluids;
import net.minecraft.world.level.chunk.status.ChunkStatus;
@@ -41,14 +39,14 @@ public final class InteractionFlowRegression {
level.setBlockAndUpdate(new BlockPos(x, 100, z), Blocks.AIR.defaultBlockState());
}
level.setBlockAndUpdate(SOURCE.above(3), Blocks.CHEST.defaultBlockState());
var player = new ServerPlayer(server, level, new GameProfile(UUID.randomUUID(), "Regression"),
ClientInformation.createDefault()) {
@Override public boolean hasDisconnected() { return false; }
};
// A real listener is now part of the shared dispatch's stale-connection contract.
var player = new net.neoforged.neoforge.common.util.FakePlayer(level,
new GameProfile(UUID.randomUUID(), "Regression"));
startTime = level.getGameTime();
DimThreadCore.MANAGER.setPaused(server, false);
SableAssemblyDispatch.dispatch(player, () -> {
if (!server.isSameThread() || !SableMainThreadBridge.ownsHandoff(level))
if (!server.isSameThread() || !PayloadMainThreadDispatch.owns(server)
|| DimThreadCore.MANAGER.ownershipLoopsSnapshot().stream().anyMatch(loop -> loop.isTicking()))
throw new AssertionError("Interaction did not obtain exclusive server ownership");
if (level.getBlockEntity(SOURCE.above(3)) == null)
throw new AssertionError("Main-thread block entity lookup silently failed");
+200
View File
@@ -0,0 +1,200 @@
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");
}
}
@@ -0,0 +1,78 @@
package dev.modzuozhi;
import net.minecraft.core.*;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.phys.*;
import net.neoforged.neoforge.network.handling.ServerPayloadContext;
/** Actual optional mod handlers, invoked through the shared context. No production dependencies. */
public final class ModInteractionRegression {
public static void run(MinecraftServer server,ServerPayloadContext ctx) throws Exception {
if(!net.neoforged.fml.ModList.get().isLoaded("create"))return;
var level=server.overworld();var pos=new BlockPos(12,115,8);var player=ctx.player();
player.setPos(12,114,8);
var block=BuiltInRegistries.BLOCK.get(ResourceLocation.parse("create:rotation_speed_controller"));
level.setBlockAndUpdate(pos,block.defaultBlockState());var be=level.getBlockEntity(pos);
PayloadRegression.check(be!=null,"Create speed controller fixture");
var speed=be.getClass().getField("targetSpeed").get(be);
var valueType=Class.forName("com.simibubi.create.foundation.blockEntity.behaviour.ValueSettingsBehaviour");
int netId=(int)SimulatedHandleRegression.call(valueType,speed,"netId",int.class,new Class<?>[]{});
var packetType=Class.forName("com.simibubi.create.foundation.blockEntity.behaviour.ValueSettingsPacket");
var hit=new BlockHitResult(Vec3.atCenterOf(pos),Direction.UP,pos,false);
var packet=packetType.getConstructor(BlockPos.class,int.class,int.class,net.minecraft.world.InteractionHand.class,
BlockHitResult.class,Direction.class,boolean.class,int.class).newInstance(pos,0,32,null,hit,Direction.UP,false,netId);
var packetBase=Class.forName("com.simibubi.create.foundation.networking.BlockEntityConfigurationPacket");
var speedType=Class.forName("com.simibubi.create.foundation.blockEntity.behaviour.scrollValue.ScrollValueBehaviour");
try {
ctx.enqueueWork(() -> {
try {SimulatedHandleRegression.call(packetBase,packet,"handle",void.class,new Class<?>[]{ServerPlayer.class},player);}
catch(Exception e){throw new RuntimeException(e);}
}).join();
int configured=(int)SimulatedHandleRegression.call(speedType,speed,"getValue",int.class,new Class<?>[]{});
PayloadRegression.check(Math.abs(configured)==32,"actual Create speed packet failed: "+configured);
player.setPos(10000,115,10000);
var denied=packetType.getConstructor(BlockPos.class,int.class,int.class,net.minecraft.world.InteractionHand.class,
BlockHitResult.class,Direction.class,boolean.class,int.class).newInstance(pos,0,64,null,hit,Direction.UP,false,netId);
ctx.enqueueWork(() -> {
try {SimulatedHandleRegression.call(packetBase,denied,"handle",void.class,new Class<?>[]{ServerPlayer.class},player);}
catch(Exception e){throw new RuntimeException(e);}
}).join();
PayloadRegression.check((int)SimulatedHandleRegression.call(speedType,speed,"getValue",int.class,new Class<?>[]{})==configured,"Create distance check bypassed");
ModZuozhi.LOGGER.info("CREATE CONFIG PASS: real speed packet and original distance denial");
player.setPos(12,114,8);
if(net.neoforged.fml.ModList.get().isLoaded("jade"))jade(ctx,pos,be,hit);
} finally {level.setBlockAndUpdate(pos,net.minecraft.world.level.block.Blocks.AIR.defaultBlockState());}
}
static void jade(ServerPayloadContext ctx,BlockPos pos,Object be,BlockHitResult hit) throws Exception {
var dataType=Class.forName("snownee.jade.impl.BlockAccessorImpl$SyncData");
var data=dataType.getConstructor(boolean.class,BlockHitResult.class,net.minecraft.world.level.block.state.BlockState.class,net.minecraft.world.item.ItemStack.class)
.newInstance(false,hit,ctx.player().level().getBlockState(pos),net.minecraft.world.item.ItemStack.EMPTY);
var requestType=Class.forName("snownee.jade.network.RequestBlockPacket");
var request=requestType.getConstructor(dataType,java.util.List.class).newInstance(data,java.util.List.of());
var jadeContext=Class.forName("snownee.jade.network.ServerPayloadContext");
var proxy=java.lang.reflect.Proxy.newProxyInstance(jadeContext.getClassLoader(),new Class<?>[]{jadeContext},(self,method,args) -> {
if(method.getName().equals("player"))return ctx.player();
return java.lang.reflect.InvocationHandler.invokeDefault(self,method,args);
});
var accessorType=Class.forName("snownee.jade.api.BlockAccessor");
var accessorImpl=Class.forName("snownee.jade.impl.BlockAccessorImpl");
var seen=new java.util.concurrent.atomic.AtomicInteger();
java.util.function.Consumer<net.minecraft.nbt.CompoundTag> consumer=tag -> {
PayloadRegression.check(tag.getInt("x")==pos.getX(),"Jade response coordinate");seen.incrementAndGet();
};
Runnable query=() -> {
try {
var accessor=SimulatedHandleRegression.call(dataType,data,"unpack",accessorType,new Class<?>[]{ServerPlayer.class},ctx.player());
PayloadRegression.check(SimulatedHandleRegression.call(accessorType,accessor,"getBlockEntity",net.minecraft.world.level.block.entity.BlockEntity.class,new Class<?>[]{})==be,"Jade BE identity");
SimulatedHandleRegression.call(accessorImpl,null,"handleRequest",void.class,new Class<?>[]{requestType,jadeContext,java.util.function.Consumer.class},request,proxy,consumer);
} catch(Exception e){throw new RuntimeException(e);}
};
ctx.enqueueWork(query).join();PayloadRegression.check(seen.get()==1,"Jade query callback missing");
ctx.player().setPos(10000,115,10000);ctx.enqueueWork(query).join();
PayloadRegression.check(seen.get()==1,"Jade range guard bypassed");
ModZuozhi.LOGGER.info("JADE REQUEST PASS: real lookup/execute/query and original range denial");
}
}
+200
View File
@@ -0,0 +1,200 @@
package dev.modzuozhi;
import dev.modzuozhi.core.dimthread.DimThreadCore;
import dev.modzuozhi.core.compat.SableMainThreadBridge;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.tick.ServerTickEvent;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
@EventBusSubscriber(modid="dimensionalripper")
public final class PayloadRegression {
static boolean ran;
static void check(boolean value, String reason) { if(!value) throw new AssertionError(reason); }
@SubscribeEvent
public static void tick(ServerTickEvent.Post event) throws Exception {
var server=event.getServer();
if(ran || server.getTickCount()<20) return;
ran=true;
if(Boolean.getBoolean("dimensionalripper.reviewRegression")) {
try {ReviewRegression.run(server);} finally {DimThreadCore.MANAGER.setPaused(server,true);server.halt(false);}
return;
}
var manager=DimThreadCore.MANAGER; var level=server.overworld();
var release=new CountDownLatch(1); var entered=new CountDownLatch(1);
var failure=new AtomicReference<Throwable>();
try {
String idleStats=dev.modzuozhi.core.compat.PayloadDiagnostics.snapshot(server);
for(int i=0;i<100;i++) dev.modzuozhi.core.compat.PayloadMainThreadDispatch.drain(server);
check(idleStats.equals(dev.modzuozhi.core.compat.PayloadDiagnostics.snapshot(server)),"empty drain created payload barrier/statistics");
ModZuozhi.LOGGER.info("PAYLOAD IDLE PASS: 100 empty drains created no payload batches");
manager.setPaused(server,true); level.setChunkForced(0,0,true); level.getChunk(0,0);
var pos=new BlockPos(8,115,8); level.setBlockAndUpdate(pos,Blocks.CHEST.defaultBlockState());
var chest=level.getBlockEntity(pos); check(chest!=null,"fixture chest");
manager.getLoop(level).execute(() -> {
server.execute(release::countDown); entered.countDown();
try {
check(release.await(5,TimeUnit.SECONDS),"callback not pumped");
SableMainThreadBridge.runForLevel(level, () -> check(level.getBlockEntity(pos)==chest,"handoff lookup"));
} catch(Throwable e) { failure.set(e); }
});
manager.setPaused(server,false); check(entered.await(5,TimeUnit.SECONDS),"worker start");
check(level.getBlockEntity(pos)==null,"must reproduce guarded null before phase");
manager.runMainThreadPhase(server,"payload-test",() -> {
check(server.isSameThread() && level.getBlockEntity(pos)==chest,"exclusive lookup");
manager.runMainThreadPhase(server,"nested",() -> check(level.getBlockEntity(pos)==chest,"nested lookup"));
// Regression: a normal resume during exclusive handling must not drop its lease.
manager.setPaused(server,false);
manager.getLoop(level).scheduleIfNeeded();
try { Thread.sleep(100); } catch(InterruptedException e) { throw new RuntimeException(e); }
check(!manager.getLoop(level).isTicking() && level.getBlockEntity(pos)==chest,"menu resume broke exclusive lease");
});
check(failure.get()==null,"Sable callback failed: "+failure.get());
manager.setPaused(server,true);
manager.runMainThreadPhase(server,"pre-paused",() -> {});
check(manager.getLoop(level).isPaused(),"pre-pause lost");
var expected=new IllegalStateException("sentinel");
try { manager.runMainThreadPhase(server,"failure",() -> {throw expected;}); throw new AssertionError("swallowed"); }
catch(IllegalStateException e) {check(e==expected,"exception identity");}
check(manager.getLoop(level).isPaused(),"exception lost pre-pause");
ModZuozhi.LOGGER.info("PAYLOAD PHASE PASS");
entry(server, pos, chest);
} finally { release.countDown(); manager.setPaused(server,true); server.halt(false); }
}
static void entry(net.minecraft.server.MinecraftServer server, BlockPos pos,
net.minecraft.world.level.block.entity.BlockEntity chest) throws Exception {
var manager=DimThreadCore.MANAGER; var level=server.overworld();
var player=new net.neoforged.neoforge.common.util.FakePlayer(level,
new com.mojang.authlib.GameProfile(java.util.UUID.randomUUID(), "PayloadTest"));
var ctx=new net.neoforged.neoforge.network.handling.ServerPayloadContext(player.connection,
net.minecraft.resources.ResourceLocation.parse("dimensionalripper:test"));
var entered=new CountDownLatch(1); var release=new CountDownLatch(1);
manager.getLoop(level).execute(() -> {
server.execute(release::countDown); entered.countDown();
try { check(release.await(5,TimeUnit.SECONDS),"entry callback pump"); }
catch(InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); }
});
manager.setPaused(server,false); check(entered.await(5,TimeUnit.SECONDS),"entry worker start");
try {
var future=ctx.enqueueWork(() -> {
check(server.isSameThread(),"payload ran off main");
check(level.getBlockEntity(pos)==chest,"public payload entry returned guarded null");
return ctx.enqueueWork(() -> 42).join();
});
check(future.join()==42,"nested supplier result");
var expected=new IllegalArgumentException("payload sentinel");
try { ctx.enqueueWork((Runnable) () -> { throw expected; }); throw new AssertionError("main exception hidden"); }
catch(IllegalArgumentException e) { check(e==expected,"main exception identity"); }
ModZuozhi.LOGGER.info("PAYLOAD ENTRY PASS");
asyncEntry(server,ctx,player);
manager.setPaused(server,false);
var assembled=new AtomicInteger();
dev.modzuozhi.core.compat.SableAssemblyDispatch.dispatch(player,() -> {
check(server.isSameThread(),"legacy assembly not main");
check(manager.loopsSnapshot().values().stream().noneMatch(loop -> loop.isTicking()),"legacy assembly lacks global ownership");
assembled.incrementAndGet();
});
check(assembled.get()==1,"legacy dispatch did not use common phase");
manager.setPaused(server,true);
ModInteractionRegression.run(server,ctx);
SimulatedHandleRegression.run(server,ctx);
var lifecyclePlayer=new net.neoforged.neoforge.common.util.FakePlayer(level,
new com.mojang.authlib.GameProfile(java.util.UUID.randomUUID(),"LifecycleTest"));
lifecycle(server,new net.neoforged.neoforge.network.handling.ServerPayloadContext(lifecyclePlayer.connection,ctx.payloadId()),lifecyclePlayer);
} finally { release.countDown(); manager.setPaused(server,true); }
}
static void asyncEntry(net.minecraft.server.MinecraftServer server,
net.neoforged.neoforge.network.handling.ServerPayloadContext ctx,
net.minecraft.server.level.ServerPlayer player) throws Exception {
var seen=new java.util.ArrayList<Integer>();
var fs=new java.util.ArrayList<CompletableFuture<Integer>>();
Thread producer=new Thread(() -> {
for(int i=0;i<100;i++) {int n=i; fs.add(ctx.enqueueWork(() -> {check(server.isSameThread(),"async handler main");seen.add(n);return n;}));}
},"payload-network-fixture");
producer.start(); producer.join(5000); check(!producer.isAlive(),"network submit blocks");
check(seen.isEmpty(),"off-thread handler executed inline");
while(fs.stream().anyMatch(f -> !f.isDone())) dev.modzuozhi.core.compat.PayloadMainThreadDispatch.drain(server);
check(seen.size()==100,"async handler count");
for(int i=0;i<100;i++) check(seen.get(i)==i && fs.get(i).join()==i,"async FIFO/result");
var cancelled=new AtomicReference<CompletableFuture<Integer>>();var changed=new AtomicInteger();
Thread cancelProducer=new Thread(() -> cancelled.set(ctx.enqueueWork(changed::incrementAndGet)));
cancelProducer.start();cancelProducer.join(5000);cancelled.get().cancel(false);
dev.modzuozhi.core.compat.PayloadMainThreadDispatch.drain(server);
check(changed.get()==0,"cancelled public future still mutated world");
var configListener=new net.minecraft.server.network.ServerConfigurationPacketListenerImpl(server,
new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND),
net.minecraft.server.network.CommonListenerCookie.createInitial(player.getGameProfile(),false));
var configCtx=new net.neoforged.neoforge.network.handling.ServerPayloadContext(configListener,ctx.payloadId());
check(configCtx.enqueueWork(() -> 17).join()==17,"configuration phase not preserved");
var guarded=new AtomicReference<CompletableFuture<Integer>>();
Thread failing=new Thread(() -> guarded.set(ctx.enqueueWork(() -> {throw new IllegalArgumentException("expected test failure");})));
failing.start();failing.join(5000);dev.modzuozhi.core.compat.PayloadMainThreadDispatch.drain(server);
check(guarded.get().join()==null,"NeoForge async handler failure contract changed");
var raw=new AtomicReference<CompletableFuture<Integer>>();
Thread stale=new Thread(() -> raw.set(dev.modzuozhi.core.compat.PayloadMainThreadDispatch.submit(player,ctx.payloadId(),() -> {throw new AssertionError("stale handler executed");})));
stale.start();stale.join(5000);
var old=player.serverLevel(); player.setServerLevel(server.getLevel(net.minecraft.world.level.Level.NETHER));
dev.modzuozhi.core.compat.PayloadMainThreadDispatch.drain(server);
check(raw.get().isCompletedExceptionally(),"stale future pending"); player.setServerLevel(old);
check(dev.modzuozhi.core.compat.PayloadDiagnostics.snapshot(server).contains("expired=1"),"stale work not counted separately");
var nested=new AtomicReference<Throwable>(); var entered=new CountDownLatch(1);
var manager=DimThreadCore.MANAGER;
manager.setPaused(server,true);
manager.getLoop(old).execute(() -> {
entered.countDown();
SableMainThreadBridge.runForLevel(old,() -> {
try {ctx.enqueueWork(() -> 1); nested.set(new AssertionError("unsafe handoff reentry accepted"));}
catch(IllegalStateException expected) {}
});
});
manager.setPaused(server,false);check(entered.await(5,TimeUnit.SECONDS),"handoff fixture start");
manager.runMainThreadPhase(server,"handoff-test",() -> {});
check(nested.get()==null,"handoff reentry result");
manager.setPaused(server,true);
var blockedFuture=new AtomicReference<CompletableFuture<Integer>>();var waitDetected=new AtomicBoolean();
var waiting=new CountDownLatch(1);
manager.getLoop(old).execute(() -> {
CompletableFuture<Integer> f=dev.modzuozhi.core.compat.PayloadMainThreadDispatch.submit(player,ctx.payloadId(),() -> {throw new AssertionError("cancelled worker wait action ran");});
blockedFuture.set(f);waiting.countDown();
try { f.get(250,TimeUnit.MILLISECONDS); }
catch(TimeoutException expected) {waitDetected.set(true);f.cancel(false);}
catch(Exception e) {throw new RuntimeException(e);}
});
manager.setPaused(server,false);check(waiting.await(5,TimeUnit.SECONDS),"worker wait fixture start");
manager.runMainThreadPhase(server,"unsupported-worker-wait",() -> {});
check(waitDetected.get() && blockedFuture.get().isCancelled(),"worker blocking cycle not detected");
manager.setPaused(server,true);
ModZuozhi.LOGGER.info("PAYLOAD ASYNC PASS");
}
static void lifecycle(net.minecraft.server.MinecraftServer server,
net.neoforged.neoforge.network.handling.ServerPayloadContext ctx,
net.minecraft.server.level.ServerPlayer player) throws Exception {
var manager=DimThreadCore.MANAGER; var other=server.getLevel(net.minecraft.world.level.Level.NETHER);
manager.setPaused(server,true); manager.removeLoop(other);
manager.runMainThreadPhase(server,"new-loop",() -> {
var created=manager.getLoop(other);created.scheduleIfNeeded();
check(created.isPaused() && !created.isTicking(),"new loop escaped phase");
});
manager.setPaused(server,true);
// Bounded ID labels, not an unbounded record for every packet.
for(int i=0;i<1000;i++) {
dev.modzuozhi.core.compat.PayloadMainThreadDispatch.submit(player,
net.minecraft.resources.ResourceLocation.parse("dimensionalripper:test_"+i),() -> 1).join();
}
String stats=dev.modzuozhi.core.compat.PayloadDiagnostics.snapshot(server);
check(stats.contains("ids=128") && stats.contains("other="),"diagnostics ID cap: "+stats);
check(stats.contains("pending=0"),"diagnostic pending count: "+stats);
var pending=new AtomicReference<CompletableFuture<Integer>>();
Thread t=new Thread(() -> pending.set(dev.modzuozhi.core.compat.PayloadMainThreadDispatch.submit(player,ctx.payloadId(),() -> {throw new AssertionError("work after stop");})));
t.start();t.join(5000);
manager.runMainThreadPhase(server,"stop-inside-phase",() -> dev.modzuozhi.core.compat.PayloadMainThreadDispatch.stop(server));
check(pending.get().isDone() && pending.get().isCompletedExceptionally(),"stop leaves future pending");
check(manager.getLoop(other).isPaused(),"stop restored worker");
check(dev.modzuozhi.core.compat.PayloadMainThreadDispatch.submit(player,ctx.payloadId(),() -> 1).isCompletedExceptionally(),"stop accepts new work");
ModZuozhi.LOGGER.info("PAYLOAD LIFECYCLE PASS");
ModZuozhi.LOGGER.info("PAYLOAD DIAGNOSTICS PASS");
}
}
+81
View File
@@ -0,0 +1,81 @@
package dev.modzuozhi;
import dev.modzuozhi.core.compat.*;
import dev.modzuozhi.core.dimthread.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.resources.ResourceLocation;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.*;
public final class ReviewRegression {
static final List<String> failures=new ArrayList<>();
static void expect(boolean ok,String name){if(!ok){failures.add(name);ModZuozhi.LOGGER.error("REVIEW RED: {}",name);}}
public static void run(MinecraftServer server) throws Exception {
var manager=DimThreadCore.MANAGER;manager.setPaused(server,true);
var player=new net.neoforged.neoforge.common.util.FakePlayer(server.overworld(),
new com.mojang.authlib.GameProfile(UUID.randomUUID(),"ReviewTest"));
var disconnects=new AtomicInteger();
new net.minecraft.server.network.ServerGamePacketListenerImpl(server,
new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND),player,
net.minecraft.server.network.CommonListenerCookie.createInitial(player.getGameProfile(),false)) {
@Override public void disconnect(net.minecraft.network.chat.Component reason){disconnects.incrementAndGet();}
};
var ctx=new net.neoforged.neoforge.network.handling.ServerPayloadContext(player.connection,ResourceLocation.parse("dimensionalripper:review"));
var result=new AtomicReference<CompletableFuture<Integer>>();
Thread rejected=new Thread(() -> result.set(ctx.enqueueWork(() -> {throw new RejectedExecutionException("handler executor closed");})));
rejected.start();rejected.join(5000);PayloadMainThreadDispatch.drain(server);PayloadMainThreadDispatch.drain(server);
expect(disconnects.get()==0,"handler rejection must not disconnect player");
try{expect(result.get().join()==null,"guard preserves handler rejection result");}
catch(CompletionException e){expect(false,"guard preserves handler rejection result");}
Thread cancelledHandler=new Thread(() -> result.set(ctx.enqueueWork(() -> {throw new CancellationException("handler cancelled its own computation");})));
cancelledHandler.start();cancelledHandler.join(5000);PayloadMainThreadDispatch.drain(server);
expect(result.get().join()==null,"guard preserves handler cancellation result");
var overflow=new AtomicReference<CompletableFuture<Integer>>();
Thread flood=new Thread(() -> {for(int i=0;i<257;i++)overflow.set(ctx.enqueueWork(() -> 1));});
flood.start();flood.join(5000);
expect(overflow.get().isCompletedExceptionally(),"capacity rejection remains exceptional");
PayloadMainThreadDispatch.drain(server);
expect(disconnects.get()==1,"genuine capacity rejection disconnects once");
for(int i=0;i<10;i++)PayloadMainThreadDispatch.drain(server);
String counters=PayloadDiagnostics.snapshot(server);
expect(counters.contains("failed=2") && counters.contains("rejected=1"),
"diagnostics distinguish handler failures from capacity rejection");
var other=server.getLevel(net.minecraft.world.level.Level.NETHER);
manager.removeLoop(other);
manager.runMainThreadPhase(server,"review-new-loop",() -> manager.getLoop(other).scheduleIfNeeded());
expect(manager.getLoop(other).isPaused(),"new loop inherits menu pause after phase exit");
manager.setPaused(server,true);
var loop=manager.getLoop(other);var entered=new CountDownLatch(1);var release=new CountDownLatch(1);
var finished=new AtomicBoolean();
loop.execute(() -> {
server.execute(() -> manager.removeLoop(other));entered.countDown();
while(release.getCount()!=0) try{release.await();}catch(InterruptedException ignored){}
finished.set(true);
});
manager.setPaused(server,false);PayloadRegression.check(entered.await(5,TimeUnit.SECONDS),"retiring loop fixture start");
Thread releaser=new Thread(() -> {try{Thread.sleep(250);}catch(InterruptedException ignored){}release.countDown();});
releaser.start();
try{manager.runMainThreadPhase(server,"review-retire",() -> expect(finished.get() && !loop.isTicking(),"retired loop must drain before exclusive phase"));}
finally{release.countDown();releaser.join(5000);}
manager.setPaused(server,true);
var labelled=new CountDownLatch(1);var labelRelease=new CountDownLatch(1);
var late=new AtomicReference<CompletableFuture<Integer>>();
Thread submitter=new Thread(() -> {
labelled.countDown();try{labelRelease.await(5,TimeUnit.SECONDS);}catch(InterruptedException e){throw new RuntimeException(e);}
// Exact late-registration boundary after the submitter had obtained its queue.
var ticket=PayloadDiagnostics.begin(server,"dimensionalripper:shutdown_race");
PayloadDiagnostics.end(server,ticket,new CancellationException("stopped"));
late.set(PayloadMainThreadDispatch.submit(player,ctx.payloadId(),() -> {throw new AssertionError("late action");}));
});
submitter.start();PayloadRegression.check(labelled.await(5,TimeUnit.SECONDS),"late submit fixture pause");
PayloadMainThreadDispatch.stop(server);PayloadMainThreadDispatch.clear(server);labelRelease.countDown();submitter.join(5000);
expect(late.get()!=null && late.get().isCompletedExceptionally(),"late future completed");
expect(PayloadDiagnostics.snapshot(server).equals("payloads: pending=0 batches=0"),"late submission must not recreate diagnostics after clear");
if(!failures.isEmpty())throw new AssertionError(String.join("; ",failures));
ModZuozhi.LOGGER.info("PAYLOAD REVIEW PASS");
}
}
@@ -0,0 +1,101 @@
package dev.modzuozhi;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.neoforged.neoforge.network.handling.ServerPayloadContext;
/** Isolated real packet, plot block entity and native constraint lifecycle fixture. */
public final class SimulatedHandleRegression {
public static void run(MinecraftServer server, ServerPayloadContext ctx) throws Exception {
if (!net.neoforged.fml.ModList.get().isLoaded("simulated")) return;
var level=server.overworld();
var handleId=ResourceLocation.parse("simulated:iron_handle");
ModZuozhi.LOGGER.info("Handle fixture block: {}",handleId);
var block=BuiltInRegistries.BLOCK.get(handleId);
var packet=Class.forName("dev.simulated_team.simulated.network.packets.UpdatePlayerUsingHandlePacket");
var ctor=packet.getConstructor(float.class,boolean.class,BlockPos.class);
var veilType=Class.forName("foundry.veil.api.network.handler.ServerPacketContext");
var veil=Class.forName("foundry.veil.forge.network.NeoForgeServerPacketContext")
.getConstructor(net.neoforged.neoforge.network.handling.IPayloadContext.class).newInstance(ctx);
var handle=packet.getMethod("handle",veilType);
var containerType=Class.forName("dev.ryanhcode.sable.api.sublevel.SubLevelContainer");
var subType=Class.forName("dev.ryanhcode.sable.sublevel.SubLevel");
var serverSubType=Class.forName("dev.ryanhcode.sable.sublevel.ServerSubLevel");
var poseType=Class.forName("dev.ryanhcode.sable.companion.math.Pose3d");
var container=call(containerType,null,"getContainer",Class.forName("dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer"),
new Class<?>[]{net.minecraft.server.level.ServerLevel.class},level);
var sub=call(containerType,container,"allocateNewSubLevel",subType,new Class<?>[]{poseType},poseType.getConstructor().newInstance());
var plot=subType.getMethod("getPlot").invoke(sub);
var plotType=Class.forName("dev.ryanhcode.sable.sublevel.plot.LevelPlot");
var pos=((BlockPos)plotType.getMethod("getCenterBlock").invoke(plot)).atY(115);
plotType.getMethod("newEmptyChunk",net.minecraft.world.level.ChunkPos.class).invoke(plot,new net.minecraft.world.level.ChunkPos(pos));
level.setBlockAndUpdate(pos,block.defaultBlockState());
var be=level.getBlockEntity(pos);
PayloadRegression.check(be!=null && be.getClass().getName().endsWith("HandleBlockEntity"),"real handle plot fixture");
var players=be.getClass().getDeclaredField("players");players.setAccessible(true);
var systemType=Class.forName("dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem");
var system=systemType.getMethod("get",net.minecraft.world.level.Level.class).invoke(null,level);
var body=systemType.getMethod("getPhysicsHandle",serverSubType).invoke(system,sub);
var bodyType=Class.forName("dev.ryanhcode.sable.api.physics.handle.RigidBodyHandle");
var jointType=Class.forName("dev.ryanhcode.sable.api.physics.constraint.PhysicsConstraintHandle");
Object joint=null;
var helper=Class.forName("dev.ryanhcode.sable.Sable").getField("HELPER").get(null);
var global=(net.minecraft.world.phys.Vec3)call(helper.getClass(),helper,"projectOutOfSubLevel",net.minecraft.world.phys.Vec3.class,
new Class<?>[]{net.minecraft.world.level.Level.class,net.minecraft.world.phys.Vec3.class},level,net.minecraft.world.phys.Vec3.atCenterOf(pos));
var player=ctx.player();player.setPos(global.x,global.y-1,global.z);player.getAbilities().flying=true;
level.addNewPlayer(player);
var release=new java.util.concurrent.CountDownLatch(1);
try {
for(boolean remove:new boolean[]{false,false,true}) {
var request=ctor.newInstance(4.0f,remove,pos);
if(remove) {
var entered=new java.util.concurrent.CountDownLatch(1);
var manager=dev.modzuozhi.core.dimthread.DimThreadCore.MANAGER;
manager.getLoop(level).execute(() -> {
server.execute(release::countDown);entered.countDown();
try {PayloadRegression.check(release.await(5,java.util.concurrent.TimeUnit.SECONDS),"native release callback pump");}
catch(InterruptedException e){throw new RuntimeException(e);}
});
manager.setPaused(server,false);
PayloadRegression.check(entered.await(5,java.util.concurrent.TimeUnit.SECONDS),"native release worker start");
// The old unprotected path clears player holding state but leaves the joint.
handle.invoke(request,veil);
PayloadRegression.check((boolean)jointType.getMethod("isValid").invoke(joint),"unprotected release did not reproduce stranded joint");
ModZuozhi.LOGGER.info("SIMULATED NATIVE RED: unprotected release left joint valid while worker owned plot");
}
ctx.enqueueWork(() -> {
try {handle.invoke(request,veil);} catch(ReflectiveOperationException e) {throw new RuntimeException(e);}
}).join();
var map=(java.util.Map<?,?>)players.get(be);
PayloadRegression.check(remove ? map.isEmpty() : map.containsKey(ctx.player().getUUID()),"real handle grab/release state");
if(!remove) {
be.getClass().getMethod("sable$physicsTick",serverSubType,bodyType,double.class).invoke(be,sub,body,0.05d);
var holding=map.get(player.getUUID());
var jointField=holding.getClass().getDeclaredField("constraintHandle");jointField.setAccessible(true);
joint=jointField.get(holding);
PayloadRegression.check(joint!=null && (boolean)jointType.getMethod("isValid").invoke(joint),"native handle joint created");
} else {
PayloadRegression.check(joint!=null && !(boolean)jointType.getMethod("isValid").invoke(joint),"native joint survived release packet");
dev.modzuozhi.core.dimthread.DimThreadCore.MANAGER.setPaused(server,true);
}
}
ModZuozhi.LOGGER.info("SIMULATED HANDLE PACKET PASS: real packet, BE and native joint invalidated on release");
} finally {
release.countDown();
dev.modzuozhi.core.dimthread.DimThreadCore.MANAGER.setPaused(server,true);
level.setBlockAndUpdate(pos,net.minecraft.world.level.block.Blocks.AIR.defaultBlockState());
level.removePlayerImmediately(player,net.minecraft.world.entity.Entity.RemovalReason.DISCARDED);
var reasonType=Class.forName("dev.ryanhcode.sable.sublevel.storage.SubLevelRemovalReason");
call(containerType,container,"removeSubLevel",void.class,new Class<?>[]{subType,reasonType},sub,reasonType.getField("REMOVED").get(null));
}
}
static Object call(Class<?> owner,Object target,String name,Class<?> result,Class<?>[] types,Object...args) throws Exception {
var signature=java.lang.invoke.MethodType.methodType(result,types);
var lookup=java.lang.invoke.MethodHandles.publicLookup();
var method=target==null?lookup.findStatic(owner,name,signature):lookup.findVirtual(owner,name,signature);
var values=new java.util.ArrayList<Object>();if(target!=null)values.add(target);java.util.Collections.addAll(values,args);
try{return method.invokeWithArguments(values);}catch(Throwable failure){throw new RuntimeException(failure);}
}
}