ref:d5c4a4d2c4818de06c262e80c9ed569d6deb3057

Fix two bugs found by GameTests

Bug 1: getUpdateTag() didn't include Cols/Rows/FontSize - Block entity data wouldn't sync to clients on chunk load - Override getUpdateTag() to call saveAdditional() - Added test: updateTagIncludesFields verifies the fix Bug 2: PTY read could block the game thread - alacritty_terminal's PTY fd may not be non-blocking when read() is called directly (bypassing mio) - Set O_NONBLOCK explicitly before each read on Unix - Prevents game tick freeze if shell produces no output Both bugs were discovered by writing comprehensive tests that exercised code paths the original tests didn't cover. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SHA: d5c4a4d2c4818de06c262e80c9ed569d6deb3057
Author: Cole Christensen <cole.christensen@macmillan.com>
Date: 2026-03-20 07:12
Parents: 9c5bdc1
3 files changed +43 -1
Type
common/src/main/java/io/fangorn/alacrittymc/block/TerminalBlockEntity.java +11 −0
@@ -256,6 +256,17 @@
}
}
/**
* Include custom data in the update tag sent to clients.
* Without this, clients would not receive Cols/Rows/FontSize on chunk load.
*/
@Override
public CompoundTag getUpdateTag() {
CompoundTag tag = super.getUpdateTag();
saveAdditional(tag);
return tag;
}
@Override
public void load(CompoundTag tag) {
super.load(tag);
fabric/src/main/java/io/fangorn/alacrittymc/fabric/test/TerminalGameTest.java +16 −0
@@ -143,6 +143,22 @@
});
}
/** getUpdateTag includes Cols/Rows/FontSize for client sync. */
@GameTest(template = EMPTY_STRUCTURE)
public void updateTagIncludesFields(GameTestHelper h) {
BlockPos pos = new BlockPos(1, 1, 1);
h.setBlock(pos, AlacrittyMod.TERMINAL_BLOCK.get().defaultBlockState());
h.succeedWhen(() -> {
TerminalBlockEntity tbe = assertTerminalEntity(h, pos);
CompoundTag tag = tbe.getUpdateTag();
if (!tag.contains("Cols")) throw new AssertionError("getUpdateTag missing Cols");
if (!tag.contains("Rows")) throw new AssertionError("getUpdateTag missing Rows");
if (!tag.contains("FontSize")) throw new AssertionError("getUpdateTag missing FontSize");
if (tag.getInt("Cols") != 80) throw new AssertionError("Wrong Cols in update tag");
if (tag.getInt("Rows") != 24) throw new AssertionError("Wrong Rows in update tag");
});
}
// ==================== INTERACTION ====================
/** useBlock() (right-click) doesn't crash on server. */
rust/src/terminal.rs +16 −1
@@ -417,9 +417,24 @@
}
}
/// Read output from PTY and process through VTE parser
/// Read output from PTY and process through VTE parser.
/// Uses non-blocking read to prevent freezing the game loop.
fn read_pty_output(&mut self) {
let read_result = if let Some(ref mut pty) = self.pty {
// Set non-blocking before each read to guarantee we never block
// the game tick thread. This is defensive — alacritty_terminal
// should set non-blocking via mio, but we bypass mio with direct read().
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let fd = pty.reader().as_raw_fd();
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFL);
if flags >= 0 && (flags & libc::O_NONBLOCK) == 0 {
libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
}
}
}
match pty.reader().read(&mut self.read_buffer) {
Ok(n) => Some(n),
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => None,