ref:e55101dfc46e9306b97cca931247b177c46d6440

Fix freeze/crash on block break: async terminal close, no cascade rescan

Freeze fix: NativeTerminal.close() now runs nativeDestroy() on a daemon background thread. The PTY child process can take time to die (SIGHUP handling), and blocking the game thread froze the entire game. Crash fix: onBlockRemoved no longer cascades rescanGroup() to all neighbors synchronously. Instead, it clears stale group refs and resets the lazy scan timer — neighbors will rescan on their next client tick. This avoids crashes from world state being inconsistent during block breaking. onPlace also simplified: only rescans the newly placed block, not all 6 neighbors. Neighbors pick up changes via lazy client tick. All operations wrapped in try-catch to prevent any exception during group management from crashing the game. 46 GameTests + 52 Rust tests, all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SHA: e55101dfc46e9306b97cca931247b177c46d6440
Author: Cole Christensen <cole.christensen@macmillan.com>
Date: 2026-03-20 14:51
Parents: 692aaa6
5 files changed +75 -39
Type
common/src/main/java/io/fangorn/alacrittymc/block/TerminalBlock.java +9 −24
@@ -97,32 +97,17 @@
@Override
public void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean movedByPiston) {
super.onPlace(state, level, pos, oldState, movedByPiston);
System.out.println("[AlacrittyMC] onPlace at " + pos + " side=" + (level.isClientSide() ? "CLIENT" : "SERVER"));
BlockEntity be = level.getBlockEntity(pos);
if (be instanceof TerminalBlockEntity terminalBE) {
terminalBE.rescanGroup();
System.out.println("[AlacrittyMC] self rescan: group=" + (terminalBE.getScreenGroup() != null
? terminalBE.getScreenGroup().getGridCols() + "x" + terminalBE.getScreenGroup().getGridRows()
: "none") + " ext=" + terminalBE.isExtension() + " cols=" + terminalBE.getCols());
} else {
System.out.println("[AlacrittyMC] NO block entity at " + pos);
// Rescan this block only — neighbors will pick up changes via lazy client tick.
// Don't cascade rescans here: it causes O(n²) work and can interact badly
// with world modification during block placement.
try {
BlockEntity be = level.getBlockEntity(pos);
if (be instanceof TerminalBlockEntity terminalBE) {
terminalBE.rescanGroup();
}
int neighborCount = 0;
for (Direction dir : new Direction[]{Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST, Direction.UP, Direction.DOWN}) {
BlockPos neighbor = pos.relative(dir);
BlockEntity neighborBE = level.getBlockEntity(neighbor);
if (neighborBE instanceof TerminalBlockEntity neighborTBE) {
neighborTBE.rescanGroup();
neighborCount++;
System.out.println("[AlacrittyMC] neighbor " + dir + " at " + neighbor
+ ": group=" + (neighborTBE.getScreenGroup() != null
? neighborTBE.getScreenGroup().getGridCols() + "x" + neighborTBE.getScreenGroup().getGridRows()
: "none") + " ext=" + neighborTBE.isExtension() + " cols=" + neighborTBE.getCols());
}
} catch (Exception e) {
System.err.println("[AlacrittyMC] Error in onPlace rescan: " + e);
}
System.out.println("[AlacrittyMC] " + neighborCount + " terminal neighbors found");
}
@Override
common/src/main/java/io/fangorn/alacrittymc/block/TerminalBlockEntity.java +24 −10
@@ -237,27 +237,41 @@
/**
* Called when this block is broken. Stops terminal, notifies neighbors.
* All operations are wrapped in try-catch to prevent crashes from
* cascading group updates during world modification.
*/
public void onBlockRemoved() {
try {
stopTerminal();
} catch (Exception e) {
stopTerminal();
System.err.println("[AlacrittyMC] Error stopping terminal: " + e);
}
// Capture and clear group state BEFORE notifying neighbors
ScreenGroup group = this.screenGroup;
this.screenGroup = null;
this.controllerPos = null;
// Notify remaining group members — clear their stale refs and let
// Notify remaining group members to reform without this block
// the lazy client tick rescan later (don't do cascading rescans now,
// as the world state may be inconsistent during block breaking)
if (level != null && group != null) {
for (BlockPos memberPos : group.getMembers()) {
if (memberPos.equals(getBlockPos())) continue;
BlockEntity be = level.getBlockEntity(memberPos);
if (be instanceof TerminalBlockEntity member) {
// Clear stale group references first
member.screenGroup = null;
member.controllerPos = null;
member.clientGroupScanned = false;
// Then rescan to form new group(s) without the broken block
member.rescanGroup();
try {
BlockEntity be = level.getBlockEntity(memberPos);
if (be instanceof TerminalBlockEntity member) {
// Clear stale refs — lazy tick will rescan
member.screenGroup = null;
member.controllerPos = null;
member.clientGroupScanned = false;
member.clientTickCount = 0;
// If this member was running a terminal (it was
// the controller), the terminal is now orphaned.
// Don't stop it here — let the rescan handle it.
}
} catch (Exception e) {
System.err.println("[AlacrittyMC] Error notifying neighbor at " + memberPos + ": " + e);
}
}
}