ref:b65660af5e136caf427d684ae140fce232d143dd

Add 6 multi-block wiring tests, fix onPlace to run on both sides

Tests verify the actual block entity behavior when groups form: - rescanAssignsControllerAndExtension: roles assigned correctly - extensionDelegatesToController: getController() resolves properly - controllerDimensionsMatchGroup: 2x1 → 80x12 - group2x2Dimensions: 2x2 → 80x24 - group3x2Dimensions: 3x2 → 120x24 - breakMiddleResetsToDefaults: survivors revert to 80x24 Fixed onPlace to run rescanGroup on both client and server (was client-only, which meant GameTests couldn't verify group formation). 34 GameTests + 52 Rust tests = 86 in-engine tests, all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SHA: b65660af5e136caf427d684ae140fce232d143dd
Author: Cole Christensen <cole.christensen@macmillan.com>
Date: 2026-03-20 07:58
Parents: d5c4a4d
2 files changed +165 -1
Type
common/src/main/java/io/fangorn/alacrittymc/block/TerminalBlock.java +1 −1
@@ -98,7 +98,7 @@
public void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean movedByPiston) {
super.onPlace(state, level, pos, oldState, movedByPiston);
// Scan for multi-block groups when a terminal block is placed
if (!level.isClientSide()) return; // Group scanning is client-side for rendering
// Runs on both client (for rendering) and server (for group state tracking)
BlockEntity be = level.getBlockEntity(pos);
if (be instanceof TerminalBlockEntity terminalBE) {
terminalBE.rescanGroup();
fabric/src/main/java/io/fangorn/alacrittymc/fabric/test/TerminalGameTest.java +164 −0
@@ -485,6 +485,170 @@
});
}
// ==================== MULTI-BLOCK WIRING (block entity integration) ====================
/** rescanGroup() assigns controller and extension roles in a 2x1. */
@GameTest(template = EMPTY_STRUCTURE, timeoutTicks = 40)
public void rescanAssignsControllerAndExtension(GameTestHelper h) {
BlockPos left = new BlockPos(1, 1, 1);
BlockPos right = new BlockPos(2, 1, 1);
h.setBlock(left, terminalState(Direction.NORTH));
h.runAfterDelay(3, () -> {
h.setBlock(right, terminalState(Direction.NORTH));
h.runAfterDelay(5, () -> {
TerminalBlockEntity beL = assertTerminalEntity(h, left);
TerminalBlockEntity beR = assertTerminalEntity(h, right);
// Trigger rescan on left (simulates onPlace)
beL.rescanGroup();
// One should be controller, the other extension
boolean leftIsCtrl = !beL.isExtension();
boolean rightIsCtrl = !beR.isExtension();
if (leftIsCtrl == rightIsCtrl) {
throw new AssertionError("One must be controller, other extension. leftExt="
+ beL.isExtension() + " rightExt=" + beR.isExtension());
}
// Both should have a screen group
if (beL.getScreenGroup() == null) throw new AssertionError("Left has no group");
if (beR.getScreenGroup() == null) throw new AssertionError("Right has no group");
h.succeed();
});
});
}
/** Extension block's getController() returns the controller entity. */
@GameTest(template = EMPTY_STRUCTURE, timeoutTicks = 40)
public void extensionDelegatesToController(GameTestHelper h) {
BlockPos a = new BlockPos(1, 1, 1);
BlockPos b = new BlockPos(2, 1, 1);
h.setBlock(a, terminalState(Direction.NORTH));
h.runAfterDelay(3, () -> {
h.setBlock(b, terminalState(Direction.NORTH));
h.runAfterDelay(5, () -> {
TerminalBlockEntity beA = assertTerminalEntity(h, a);
beA.rescanGroup();
TerminalBlockEntity beB = assertTerminalEntity(h, b);
// Find which is the extension
TerminalBlockEntity extension = beA.isExtension() ? beA : beB;
TerminalBlockEntity controller = beA.isExtension() ? beB : beA;
// Extension's getController() should return the controller
TerminalBlockEntity resolved = extension.getController();
if (resolved != controller) {
throw new AssertionError("Extension.getController() returned wrong entity");
}
h.succeed();
});
});
}
/** Controller cols/rows update to combined dimensions after rescan. */
@GameTest(template = EMPTY_STRUCTURE, timeoutTicks = 40)
public void controllerDimensionsMatchGroup(GameTestHelper h) {
// 2x1 group: should be 2*COLS_PER_BLOCK wide, 1*ROWS_PER_BLOCK tall
BlockPos a = new BlockPos(1, 1, 1);
BlockPos b = new BlockPos(2, 1, 1);
h.setBlock(a, terminalState(Direction.NORTH));
h.setBlock(b, terminalState(Direction.NORTH));
h.runAfterDelay(5, () -> {
TerminalBlockEntity beA = assertTerminalEntity(h, a);
TerminalBlockEntity beB = assertTerminalEntity(h, b);
beA.rescanGroup();
TerminalBlockEntity controller = beA.isExtension() ? beB : beA;
// COLS_PER_BLOCK=40, ROWS_PER_BLOCK=12 → 2*40=80, 1*12=12
if (controller.getCols() != 80) {
throw new AssertionError("Expected 80 cols, got " + controller.getCols());
}
if (controller.getRows() != 12) {
throw new AssertionError("Expected 12 rows, got " + controller.getRows());
}
h.succeed();
});
}
/** 2x2 group has 80 cols and 24 rows. */
@GameTest(template = EMPTY_STRUCTURE, timeoutTicks = 40)
public void group2x2Dimensions(GameTestHelper h) {
for (int x = 1; x <= 2; x++)
for (int y = 1; y <= 2; y++)
h.setBlock(new BlockPos(x, y, 1), terminalState(Direction.NORTH));
h.runAfterDelay(5, () -> {
// Rescan from each block to ensure all are in the group
for (int x = 1; x <= 2; x++)
for (int y = 1; y <= 2; y++)
assertTerminalEntity(h, new BlockPos(x, y, 1)).rescanGroup();
// Find any controller
TerminalBlockEntity be = assertTerminalEntity(h, new BlockPos(1, 1, 1));
TerminalBlockEntity ctrl = be.isExtension() ? be.getController() : be;
// 2*40=80 cols, 2*12=24 rows
if (ctrl.getCols() != 80) throw new AssertionError("Cols=" + ctrl.getCols() + ", expected 80");
if (ctrl.getRows() != 24) throw new AssertionError("Rows=" + ctrl.getRows() + ", expected 24");
h.succeed();
});
}
/** 3x2 group has 120 cols and 24 rows. */
@GameTest(template = EMPTY_STRUCTURE, timeoutTicks = 40)
public void group3x2Dimensions(GameTestHelper h) {
for (int x = 1; x <= 3; x++)
for (int y = 1; y <= 2; y++)
h.setBlock(new BlockPos(x, y, 1), terminalState(Direction.NORTH));
h.runAfterDelay(5, () -> {
// Rescan from each block
for (int x = 1; x <= 3; x++)
for (int y = 1; y <= 2; y++)
assertTerminalEntity(h, new BlockPos(x, y, 1)).rescanGroup();
TerminalBlockEntity be = assertTerminalEntity(h, new BlockPos(1, 1, 1));
TerminalBlockEntity ctrl = be.isExtension() ? be.getController() : be;
// 3*40=120 cols, 2*12=24 rows
if (ctrl.getCols() != 120) throw new AssertionError("Cols=" + ctrl.getCols() + ", expected 120");
if (ctrl.getRows() != 24) throw new AssertionError("Rows=" + ctrl.getRows() + ", expected 24");
h.succeed();
});
}
/** Breaking middle of 3x1 leaves two singles with default 80x24. */
@GameTest(template = EMPTY_STRUCTURE, timeoutTicks = 60)
public void breakMiddleResetsToDefaults(GameTestHelper h) {
BlockPos a = new BlockPos(1, 1, 1);
BlockPos b = new BlockPos(2, 1, 1);
BlockPos c = new BlockPos(3, 1, 1);
h.setBlock(a, terminalState(Direction.NORTH));
h.setBlock(b, terminalState(Direction.NORTH));
h.setBlock(c, terminalState(Direction.NORTH));
h.runAfterDelay(5, () -> {
// Form group
assertTerminalEntity(h, a).rescanGroup();
h.runAfterDelay(3, () -> {
// Break middle
h.destroyBlock(b);
h.runAfterDelay(5, () -> {
// Remaining blocks should be standalone with default dims
TerminalBlockEntity beA = assertTerminalEntity(h, a);
TerminalBlockEntity beC = assertTerminalEntity(h, c);
// After neighbor notification, they should have rescanned
if (beA.isExtension()) throw new AssertionError("A should not be extension after break");
if (beC.isExtension()) throw new AssertionError("C should not be extension after break");
if (beA.getCols() != 80) throw new AssertionError("A cols=" + beA.getCols() + " expected 80");
if (beC.getCols() != 80) throw new AssertionError("C cols=" + beC.getCols() + " expected 80");
h.succeed();
});
});
});
}
// ==================== HELPERS ====================
private static BlockState terminalState(Direction facing) {