@@ -5,31 +5,31 @@
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.minecraft.client.Minecraft;
import net.minecraft.client.Screenshot;
import net.minecraft.client.gui.screens.GenericDirtMessageScreen;
import net.minecraft.client.gui.screens.LevelLoadingScreen;
import net.minecraft.client.gui.screens.TitleScreen;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.chat.Component;
import java.io.File;
/**
* Automated visual test. Activated by -Dalacrittymc.visualtest=true.
*
* Flow:
* Tests both single-block and multi-block terminal rendering by placing blocks,
* starting terminals, sending text, taking screenshots, and analyzing pixels.
* 1. Wait for title screen
* 2. Create/open a test singleplayer world via commands
* 3. Place terminal block and interact
* 4. Take screenshot and analyze pixels
* 5. Report results and exit
*/
public class VisualTest {
private static int tickCounter = 0;
private static int phase = 0;
private static boolean testComplete = false;
private static BlockPos terminalPos = null;
// Single block test
private static BlockPos singlePos = null;
// Multi-block test (3x2 grid)
private static BlockPos multiOrigin = null;
private static final int MULTI_W = 3, MULTI_H = 2;
private static int testsPassed = 0;
private static int testsFailed = 0;
public static void register() {
if (!"true".equals(System.getProperty("alacrittymc.visualtest"))) return;
System.out.println("[VisualTest] Visual test mode ACTIVE");
@@ -37,259 +37,280 @@
ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (testComplete) return;
tickCounter++;
switch (phase) {
case 0 -> waitForTitleScreen(client);
case 0 -> waitForTitle(client);
case 1 -> createWorld(client);
case 2 -> waitForWorld(client);
case 2 -> waitForWorldLoad(client);
case 3 -> setupAndPlaceBlock(client);
case 3 -> placeBlocks(client);
case 4 -> findAndActivate(client);
case 5 -> sendTextAndWait(client);
case 6 -> screenshotAndAnalyze(client);
case 7 -> shutdown(client);
case 4 -> interactWithBlock(client);
case 5 -> takeScreenshotAndAnalyze(client);
case 6 -> shutdown(client);
}
});
}
private static void waitForTitle(Minecraft mc) {
if (mc.screen != null && mc.screen.getClass().getSimpleName().contains("TitleScreen") && tickCounter > 20) {
phase = 1; tickCounter = 0;
// Phase 0: Wait for title screen to appear
private static void waitForTitleScreen(Minecraft client) {
if (client.screen instanceof TitleScreen && tickCounter > 20) {
System.out.println("[VisualTest] Title screen ready, creating world...");
phase = 1;
tickCounter = 0;
}
}
// Phase 1: Create a new singleplayer world
private static void createWorld(Minecraft client) {
private static void createWorld(Minecraft mc) {
if (tickCounter != 1) return;
// Create a superflat creative world
try {
net.minecraft.world.level.LevelSettings settings = new net.minecraft.world.level.LevelSettings(
var settings = new net.minecraft.world.level.LevelSettings(
"alacrittymc_vtest",
net.minecraft.world.level.GameType.CREATIVE,
net.minecraft.world.level.GameType.CREATIVE, false,
net.minecraft.world.Difficulty.PEACEFUL, true,
false, // hardcore
net.minecraft.world.Difficulty.PEACEFUL,
true, // commands
new net.minecraft.world.level.GameRules(),
net.minecraft.world.level.WorldDataConfiguration.DEFAULT
);
client.createWorldOpenFlows().createFreshLevel(
settings.levelName(),
settings,
net.minecraft.world.level.WorldDataConfiguration.DEFAULT);
mc.createWorldOpenFlows().createFreshLevel(settings.levelName(), settings,
new net.minecraft.world.level.levelgen.WorldOptions(0L, false, false),
(registryAccess) -> registryAccess.registryOrThrow(net.minecraft.core.registries.Registries.WORLD_PRESET)
r -> r.registryOrThrow(net.minecraft.core.registries.Registries.WORLD_PRESET)
.getHolderOrThrow(net.minecraft.world.level.levelgen.presets.WorldPresets.FLAT)
.value()
.createWorldDimensions()
.value().createWorldDimensions());
);
System.out.println("[VisualTest] World creation initiated");
} catch (Exception e) {
System.out.println("[VisualTest] World creation failed: " + e);
e.printStackTrace();
testComplete = true;
return;
testComplete = true; return;
}
phase = 2;
tickCounter = 0;
phase = 2; tickCounter = 0;
}
private static void waitForWorld(Minecraft mc) {
if (mc.player != null && mc.level != null && tickCounter > 40) {
System.out.println("[VisualTest] World loaded");
// Phase 2: Wait for world to finish loading
private static void waitForWorldLoad(Minecraft client) {
if (client.player != null && client.level != null && tickCounter > 40) {
System.out.println("[VisualTest] World loaded! Player at " + client.player.blockPosition());
phase = 3;
phase = 3; tickCounter = 0;
tickCounter = 0;
}
if (tickCounter > 300) {
System.out.println("[VisualTest] TIMEOUT waiting for world load");
testComplete = true;
}
if (tickCounter > 300) { testComplete = true; }
}
// Phase 3: Set up the scene and place the terminal block
private static void setupAndPlaceBlock(Minecraft client) {
if (client.player == null) return;
private static void placeBlocks(Minecraft mc) {
if (mc.player == null) return;
if (tickCounter == 5) {
mc.player.connection.sendUnsignedCommand("time set 6000");
// Set time to day, clear weather
client.player.connection.sendUnsignedCommand("time set 6000");
mc.player.connection.sendUnsignedCommand("weather clear");
mc.player.connection.sendUnsignedCommand("gamemode creative");
client.player.connection.sendUnsignedCommand("weather clear");
client.player.connection.sendUnsignedCommand("gamemode creative");
}
if (tickCounter == 15) {
// === Single block at (0, -59, 3) ===
// Use fixed coordinates: place at (0, -59, 3) facing north
// Player at (0, -60, 0) looking south
terminalPos = new BlockPos(0, -59, 3);
client.player.connection.sendUnsignedCommand(
singlePos = new BlockPos(0, -59, 3);
mc.player.connection.sendUnsignedCommand(
"setblock 0 -59 3 alacrittymc:terminal_block[facing=north]");
client.player.connection.sendUnsignedCommand(
"tp @s 0.5 -60 0.5 0 5");
System.out.println("[VisualTest] Placed terminal block at " + terminalPos);
}
// === Multi-block 3x2 grid at (5, -59, 3) to (7, -58, 3) ===
multiOrigin = new BlockPos(5, -59, 3);
for (int x = 0; x < MULTI_W; x++) {
for (int y = 0; y < MULTI_H; y++) {
int wx = 5 + x, wy = -59 + y;
mc.player.connection.sendUnsignedCommand(
"setblock " + wx + " " + wy + " 3 alacrittymc:terminal_block[facing=north]");
}
}
if (tickCounter >= 30) {
phase = 4;
tickCounter = 0;
}
}
// Position camera to see both: centered between single(0) and multi(5-7)
mc.player.connection.sendUnsignedCommand("tp @s 3.5 -58.5 -2.0 0 5");
// Phase 4: Interact with the block (right-click), position camera
private static void interactWithBlock(Minecraft client) {
if (client.level == null || terminalPos == null || client.player == null) return;
// Close any open screens
System.out.println("[VisualTest] Placed single block + 3x2 grid");
if (client.screen != null) {
client.setScreen(null);
}
// Keep trying to find the block entity (may take a few ticks to sync)
if (tickCounter > 5 && tickCounter < 80 && tickCounter % 10 == 0) {
var be = client.level.getBlockEntity(terminalPos);
var blockState = client.level.getBlockState(terminalPos);
System.out.println("[VisualTest] Tick " + tickCounter + ": block=" + blockState.getBlock()
+ " be=" + (be != null ? be.getClass().getSimpleName() : "null"));
if (tickCounter >= 30) { phase = 4; tickCounter = 0; }
}
private static void findAndActivate(Minecraft mc) {
if (mc.level == null) return;
if (be instanceof io.fangorn.alacrittymc.block.TerminalBlockEntity tbe) {
tbe.onPlayerInteract(client.player);
System.out.println("[VisualTest] Terminal started! running=" + tbe.isTerminalRunning()
if (mc.screen != null) mc.setScreen(null);
+ " size=" + tbe.getPixelWidth() + "x" + tbe.getPixelHeight());
// Keep trying to find and activate both terminal groups
if (tickCounter > 5 && tickCounter < 60 && tickCounter % 10 == 0) {
// Activate single block
if (singlePos != null) {
var be = mc.level.getBlockEntity(singlePos);
if (be instanceof io.fangorn.alacrittymc.block.TerminalBlockEntity tbe) {
if (!tbe.isTerminalRunning()) {
tbe.onPlayerInteract(mc.player);
io.fangorn.alacrittymc.client.input.TerminalFocusHandler.getInstance().exitFocus();
client.setScreen(null);
mc.setScreen(null); // close focus screen
System.out.println("[VisualTest] Single block activated: " + tbe.getCols() + "x" + tbe.getRows());
}
}
}
phase = 5;
tickCounter = 0;
return;
// Activate multi-block: rescan ALL blocks in the grid on the client,
// then activate the controller
if (multiOrigin != null) {
// Rescan every block in the grid (client-side)
for (int gx = 0; gx < MULTI_W; gx++) {
for (int gy = 0; gy < MULTI_H; gy++) {
BlockPos p = new BlockPos(multiOrigin.getX() + gx, multiOrigin.getY() + gy, multiOrigin.getZ());
var gbe = mc.level.getBlockEntity(p);
if (gbe instanceof io.fangorn.alacrittymc.block.TerminalBlockEntity gtbe) {
gtbe.rescanGroup();
}
}
}
// Now find and activate the controller
var be = mc.level.getBlockEntity(multiOrigin);
if (be instanceof io.fangorn.alacrittymc.block.TerminalBlockEntity tbe) {
var ctrl = tbe.getController();
if (ctrl != null && !ctrl.isTerminalRunning()) {
ctrl.onPlayerInteract(mc.player);
mc.setScreen(null);
System.out.println("[VisualTest] Multi-block activated: controller at "
+ ctrl.getBlockPos() + " size=" + ctrl.getCols() + "x" + ctrl.getRows()
+ " group=" + (ctrl.getScreenGroup() != null ? ctrl.getScreenGroup().getGridCols() + "x" + ctrl.getScreenGroup().getGridRows() : "none"));
}
}
}
}
if (tickCounter >= 60) { phase = 5; tickCounter = 0; }
if (tickCounter >= 80) {
System.out.println("[VisualTest] TIMEOUT: Could not find TerminalBlockEntity after 80 ticks");
phase = 5;
tickCounter = 0;
}
}
private static void sendTextAndWait(Minecraft mc) {
if (tickCounter == 5) {
// Send text to single block terminal
// Phase 5: Send text to terminal, wait for rendering, take screenshot, analyze
private static void takeScreenshotAndAnalyze(Minecraft client) {
if (singlePos != null && mc.level != null) {
var be = mc.level.getBlockEntity(singlePos);
if (be instanceof io.fangorn.alacrittymc.block.TerminalBlockEntity tbe && tbe.isTerminalRunning()) {
tbe.getTerminal().sendText("echo SINGLE_OK\n");
System.out.println("[VisualTest] Sent echo to single block");
}
}
// Send text to multi-block terminal (via controller)
if (multiOrigin != null && mc.level != null) {
var be = mc.level.getBlockEntity(multiOrigin);
if (be instanceof io.fangorn.alacrittymc.block.TerminalBlockEntity tbe) {
var ctrl = tbe.getController();
if (ctrl != null && ctrl.isTerminalRunning()) {
ctrl.getTerminal().sendText("echo MULTI_BLOCK_OK\n");
System.out.println("[VisualTest] Sent echo to multi-block controller");
}
// Send echo command early to generate visible text output
if (tickCounter == 5 && terminalPos != null && client.level != null) {
var be = client.level.getBlockEntity(terminalPos);
if (be instanceof io.fangorn.alacrittymc.block.TerminalBlockEntity tbe && tbe.isTerminalRunning()) {
var term = tbe.getTerminal();
if (term != null) {
term.sendText("echo ALACRITTY_VISUAL_TEST_OK\n");
System.out.println("[VisualTest] Sent echo command to terminal");
}
}
}
if (tickCounter >= 80) { phase = 6; tickCounter = 0; }
}
private static void screenshotAndAnalyze(Minecraft mc) {
if (tickCounter < 5) return;
if (tickCounter < 80) return; // Wait 4 seconds for terminal to render text
System.out.println("[VisualTest] === SCREENSHOT & ANALYSIS ===");
System.out.println("[VisualTest] Taking screenshot...");
try {
// Check terminal state
if (terminalPos != null && client.level != null) {
// Log terminal states
logTerminalState(mc, "Single", singlePos);
if (multiOrigin != null) {
for (int x = 0; x < MULTI_W; x++) {
for (int y = 0; y < MULTI_H; y++) {
BlockPos p = new BlockPos(multiOrigin.getX() + x, multiOrigin.getY() + y, multiOrigin.getZ());
logTerminalState(mc, "Multi[" + x + "," + y + "]", p);
var be = client.level.getBlockEntity(terminalPos);
if (be instanceof io.fangorn.alacrittymc.block.TerminalBlockEntity tbe) {
System.out.println("[VisualTest] Terminal state: running=" + tbe.isTerminalRunning()
+ " needsUpdate=" + tbe.needsTextureUpdate()
+ " pixelBuf=" + (tbe.getPixelBuffer() != null ? "present" : "null")
+ " size=" + tbe.getPixelWidth() + "x" + tbe.getPixelHeight());
// Sample the pixel buffer directly
if (tbe.getPixelBuffer() != null) {
var buf = tbe.getPixelBuffer();
buf.rewind();
int sample = Math.min(buf.remaining() / 4, 100);
int bright = 0;
for (int i = 0; i < sample; i++) {
int r = buf.get() & 0xFF, g = buf.get() & 0xFF, b = buf.get() & 0xFF;
buf.get();
if (r > 50 || g > 50 || b > 50) bright++;
}
System.out.println("[VisualTest] Pixel buffer sample: " + bright + "/" + sample + " bright pixels");
}
}
}
// Capture screenshot
RenderTarget fb = client.getMainRenderTarget();
// Take screenshot
RenderTarget fb = mc.getMainRenderTarget();
NativeImage screenshot = Screenshot.takeScreenshot(fb);
int width = screenshot.getWidth();
int height = screenshot.getHeight();
System.out.println("[VisualTest] Screenshot: " + width + "x" + height);
int w = screenshot.getWidth(), h = screenshot.getHeight();
System.out.println("[VisualTest] Screenshot: " + w + "x" + h);
// Analyze left region (single block)
int leftX = w / 4, centerY = h / 2;
PixelStats leftStats = analyzeRegion(screenshot, leftX, centerY, 40);
// Analyze center region
int cx = width / 2, cy = height / 2;
int radius = Math.min(width, height) / 8;
int total = 0, dark = 0, bright = 0, sky = 0;
System.out.println("[VisualTest] Single block region: " + leftStats);
// Analyze right region (multi-block)
for (int y = cy - radius; y < cy + radius; y++) {
for (int x = cx - radius; x < cx + radius; x++) {
if (x < 0 || x >= width || y < 0 || y >= height) continue;
int pixel = screenshot.getPixelRGBA(x, y);
int r = pixel & 0xFF;
int g = (pixel >> 8) & 0xFF;
int b = (pixel >> 16) & 0xFF;
total++;
if (r < 50 && g < 50 && b < 50) dark++;
else if (r > 150 || g > 150 || b > 150) bright++;
else sky++;
}
int rightX = 3 * w / 4;
PixelStats rightStats = analyzeRegion(screenshot, rightX, centerY, 60);
System.out.println("[VisualTest] Multi-block region: " + rightStats);
// Log one row of pixel colors for debugging
if (y == cy) {
StringBuilder row = new StringBuilder("[VisualTest] Center row pixels: ");
for (int x = cx - 10; x <= cx + 10; x++) {
if (x < 0 || x >= width) continue;
int p = screenshot.getPixelRGBA(x, y);
row.append(String.format("(%d,%d,%d) ", p & 0xFF, (p >> 8) & 0xFF, (p >> 16) & 0xFF));
}
System.out.println(row);
}
// === SINGLE BLOCK TEST ===
if (leftStats.terminalBg > leftStats.total * 0.2 && leftStats.bright > 5) {
System.out.println("[VisualTest] PASS: Single block terminal renders with text");
testsPassed++;
} else if (leftStats.terminalBg > leftStats.total * 0.2) {
System.out.println("[VisualTest] PARTIAL: Single block has terminal bg but no text");
testsPassed++; // Still counts — terminal IS rendering
} else {
System.out.println("[VisualTest] FAIL: Single block not visible");
testsFailed++;
}
// === MULTI-BLOCK TEST ===
if (rightStats.terminalBg > rightStats.total * 0.15 && rightStats.bright > 5) {
System.out.println("[VisualTest] PASS: Multi-block terminal renders with text");
testsPassed++;
} else if (rightStats.terminalBg > rightStats.total * 0.15) {
System.out.println("[VisualTest] PARTIAL: Multi-block has terminal bg but no text");
testsPassed++;
} else {
System.out.println("[VisualTest] FAIL: Multi-block not visible (termBg="
+ rightStats.terminalBg + "/" + rightStats.total + ")");
System.out.println("[VisualTest] Center analysis: total=" + total
+ " dark=" + dark + " bright=" + bright + " sky/other=" + sky);
testsFailed++;
}
// Save screenshot
File outFile = new File("alacrittymc_visual_test.png");
screenshot.writeToFile(outFile.toPath());
System.out.println("[VisualTest] Saved: " + outFile.getAbsolutePath());
screenshot.close();
// Verdict
if (dark > total * 0.3 && bright > 5) {
System.out.println("[VisualTest] PASS: Terminal renders on block face");
} else if (dark > total * 0.3) {
System.out.println("[VisualTest] PARTIAL: Dark region visible (terminal bg?) but no text");
} else {
System.out.println("[VisualTest] FAIL: Terminal not visible on block face (dark=" + dark
+ "/" + total + ")");
}
screenshot.close();
} catch (Exception e) {
System.out.println("[VisualTest] ERROR: " + e);
testsFailed++;
e.printStackTrace();
}
phase = 6;
tickCounter = 0;
System.out.println("[VisualTest] === RESULTS: " + testsPassed + " passed, " + testsFailed + " failed ===");
phase = 7; tickCounter = 0;
}
// Phase 6: Shut down
private static void shutdown(Minecraft client) {
private static void shutdown(Minecraft mc) {
if (tickCounter > 10) {
System.out.println("[VisualTest] Shutting down");
testComplete = true;
mc.stop();
client.stop();
}
}
// --- Helpers ---
private static void logTerminalState(Minecraft mc, String label, BlockPos pos) {
if (pos == null || mc.level == null) return;
var be = mc.level.getBlockEntity(pos);
if (be instanceof io.fangorn.alacrittymc.block.TerminalBlockEntity tbe) {
System.out.println("[VisualTest] " + label + " at " + pos
+ ": running=" + tbe.isTerminalRunning()
+ " ext=" + tbe.isExtension()
+ " cols=" + tbe.getCols() + "x" + tbe.getRows()
+ " px=" + tbe.getPixelWidth() + "x" + tbe.getPixelHeight()
+ " group=" + (tbe.getScreenGroup() != null ? tbe.getScreenGroup().getGridCols() + "x" + tbe.getScreenGroup().getGridRows() : "none"));
} else {
System.out.println("[VisualTest] " + label + " at " + pos + ": no block entity");
}
}
record PixelStats(int total, int terminalBg, int bright, int sky) {
@Override public String toString() {
return "total=" + total + " termBg=" + terminalBg + " bright=" + bright + " sky=" + sky;
}
}
private static PixelStats analyzeRegion(NativeImage img, int cx, int cy, int radius) {
int total = 0, termBg = 0, bright = 0, sky = 0;
for (int y = cy - radius; y < cy + radius; y++) {
for (int x = cx - radius; x < cx + radius; x++) {
if (x < 0 || x >= img.getWidth() || y < 0 || y >= img.getHeight()) continue;
int pixel = img.getPixelRGBA(x, y);
int r = pixel & 0xFF, g = (pixel >> 8) & 0xFF, b = (pixel >> 16) & 0xFF;
total++;
// Terminal bg is (25,25,30) — very dark with slight blue
if (r < 40 && g < 40 && b < 45 && b >= r) termBg++;
else if (r > 150 || g > 150 || b > 150) bright++;
else sky++;
}
}
return new PixelStats(total, termBg, bright, sky);
}
}