package com.projectsacrifice.game; import java.io.IOException; import java.io.PrintStream; import java.io.Reader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; /** Desktop entrypoint for deterministic validation and content export. */ public final class LevelContentTool { private LevelContentTool() {} public static void main(String[] args) { int exitCode = run(args, System.out, System.err); if (exitCode != 0) throw new IllegalStateException("Level content tool failed with exit " + exitCode); } public static int run(String[] args, PrintStream output, PrintStream errors) { if (output == null || errors == null) throw new IllegalArgumentException("output streams are required"); try { if (args != null && args.length == 2 && "validate".equals(args[0])) { List levels = loadDirectory(args[1]); output.println("Validated " + levels.size() + " level(s); format=" + LevelFormat.CURRENT_FORMAT_VERSION + "; physics=" + LevelFormat.CURRENT_PHYSICS_VERSION); return 0; } if (args != null && args.length == 3 && "preview".equals(args[0])) { List levels = loadDirectory(args[1]); Path destination = Paths.get(args[2]).toAbsolutePath().normalize(); writePreview(levels, destination); output.println("Exported " + levels.size() + " level preview(s) to " + destination); return 0; } errors.println("Usage: validate | preview "); return 2; } catch (IllegalArgumentException | IOException error) { errors.println(error.getMessage()); return 1; } } private static List loadDirectory(String value) { Path directory = Paths.get(value).toAbsolutePath().normalize(); return LevelCatalogLoader.load(new DirectorySource(directory), "index.json"); } private static void writePreview(List levels, Path destination) throws IOException { Files.createDirectories(destination); StringBuilder coordinates = new StringBuilder( "level\tkind\tindex\ttype\tx\ty\twidth\theight\ttravelX\ttravelY\tcontrolOrSignals\n"); for (LevelDefinition level : levels) { Files.writeString(destination.resolve(String.format("level-%02d.svg", level.number)), svg(level), StandardCharsets.UTF_8); coordinates.append(coordinates(level)); } Files.writeString(destination.resolve("catalog.tsv"), coordinates.toString(), StandardCharsets.UTF_8); } private static String svg(LevelDefinition level) { StringBuilder value = new StringBuilder(); value.append("\n") .append("\n") .append(" ").append(xml(level.number + " — " + level.title)).append("\n") .append(" \n"); for (int i = 0; i < level.platforms.size(); i++) { rect(value, "platform", i, level.platforms.get(i).box, "#64748b"); } for (int i = 0; i < level.objects.size(); i++) { LevelDefinition.PhysicalObject item = level.objects.get(i); rect(value, "object-" + item.type, i, item.box, "#f59e0b"); } for (int i = 0; i < level.hazards.size(); i++) { rect(value, "hazard-" + level.hazards.get(i).type, i, level.hazards.get(i).box, "#ef4444"); } for (int i = 0; i < level.switches.size(); i++) { rect(value, "switch-" + level.switches.get(i).type, i, level.switches.get(i).box, "#eab308"); } for (int i = 0; i < level.doors.size(); i++) { rect(value, "door", i, level.doors.get(i).box, "#a855f7"); } rect(value, "spawn", 0, new LevelDefinition.Box(level.spawnX, level.spawnY, 62f, 100f), "#22c55e"); rect(value, "goal", 0, level.goal, "#38bdf8"); if (level.collectible != null) { rect(value, "collectible", 0, level.collectible, "#facc15"); } value.append("\n"); return value.toString(); } private static void rect(StringBuilder output, String kind, int index, LevelDefinition.Box box, String color) { output.append(" \n"); } private static String coordinates(LevelDefinition level) { StringBuilder value = new StringBuilder(); row(value, level.number, "spawn", 0, "PLAYER", new LevelDefinition.Box( level.spawnX, level.spawnY, 62f, 100f), 0f, 0f, ""); row(value, level.number, "goal", 0, "GOAL", level.goal, 0f, 0f, ""); if (level.collectible != null) { row(value, level.number, "collectible", 0, "CACHE", level.collectible, 0f, 0f, level.levelId); } for (int i = 0; i < level.platforms.size(); i++) { LevelDefinition.Platform item = level.platforms.get(i); row(value, level.number, "platform", i, item.isMoving() ? "MOVING" : "STATIC", item.box, item.travelX, item.travelY, item.controlId); } for (int i = 0; i < level.objects.size(); i++) { LevelDefinition.PhysicalObject item = level.objects.get(i); row(value, level.number, "object", i, item.type.name(), item.box, 0f, 0f, "mass=" + item.mass); } for (int i = 0; i < level.hazards.size(); i++) { LevelDefinition.Hazard item = level.hazards.get(i); row(value, level.number, "hazard", i, item.type.name(), item.box, item.impulseX, item.impulseY, item.relayId); } for (int i = 0; i < level.switches.size(); i++) { LevelDefinition.Switch item = level.switches.get(i); row(value, level.number, "switch", i, item.type.name(), item.box, 0f, 0f, item.signalId); } for (int i = 0; i < level.doors.size(); i++) { LevelDefinition.Door item = level.doors.get(i); row(value, level.number, "door", i, "DOOR", item.box, 0f, 0f, String.join("+", item.requiredSignals)); } return value.toString(); } private static void row(StringBuilder output, int level, String kind, int index, String type, LevelDefinition.Box box, float travelX, float travelY, String control) { output.append(level).append('\t').append(kind).append('\t').append(index).append('\t') .append(type).append('\t').append(number(box.x)).append('\t') .append(number(box.y)).append('\t').append(number(box.width)).append('\t') .append(number(box.height)).append('\t').append(number(travelX)).append('\t') .append(number(travelY)).append('\t').append(control == null ? "" : control).append('\n'); } private static String number(float value) { if (value == (long) value) return Long.toString((long) value); return Float.toString(value); } private static String xml(String value) { return value.replace("&", "&").replace("<", "<") .replace(">", ">").replace("\"", """); } private static final class DirectorySource implements LevelResourceSource { private final Path directory; DirectorySource(Path directory) { this.directory = directory; } @Override public Reader open(String path) throws IOException { Path resolved = directory.resolve(path).normalize(); if (!resolved.startsWith(directory) || !Files.isRegularFile(resolved)) return null; return Files.newBufferedReader(resolved, StandardCharsets.UTF_8); } } }