feat(assets): verify ready exports and fix sandbox bridge
This commit is contained in:
@@ -42,6 +42,14 @@ tasks.register<JavaExec>("validateFanContent") {
|
||||
args("validate", fanContentRoot.asFile.absolutePath)
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn("validateFanContent")
|
||||
tasks.register<JavaExec>("validateReadyFanAssets") {
|
||||
group = "verification"
|
||||
description = "Refuses READY catalog records whose Blockbench export artifacts are absent or malformed."
|
||||
classpath = sourceSets.main.get().runtimeClasspath
|
||||
mainClass = application.mainClass
|
||||
args("ready-assets", fanContentRoot.asFile.absolutePath, rootProject.layout.projectDirectory.asFile.absolutePath)
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn("validateFanContent", "validateReadyFanAssets")
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public final class ContentTool {
|
||||
}
|
||||
|
||||
static int run(String[] args, PrintStream out, PrintStream err) {
|
||||
if (args.length != 2) {
|
||||
if ((args.length != 2) && (args.length != 3 || !"ready-assets".equals(args[0]))) {
|
||||
printUsage(err);
|
||||
return 64;
|
||||
}
|
||||
@@ -24,6 +24,7 @@ public final class ContentTool {
|
||||
return switch (args[0]) {
|
||||
case "validate" -> validate(contentRoot, out, err);
|
||||
case "digest" -> digest(contentRoot, out, err);
|
||||
case "ready-assets" -> validateReadyAssets(contentRoot, Path.of(args[2]), out, err);
|
||||
default -> {
|
||||
err.println("Unknown command: " + args[0]);
|
||||
printUsage(err);
|
||||
@@ -59,9 +60,21 @@ public final class ContentTool {
|
||||
}
|
||||
}
|
||||
|
||||
private static int validateReadyAssets(Path contentRoot, Path projectRoot, PrintStream out, PrintStream err) {
|
||||
ValidationReport report = new ReadyAssetValidator().validate(contentRoot, projectRoot);
|
||||
if (!report.isValid()) {
|
||||
err.println("Ready asset validation failed with " + report.issues().size() + " issue(s):");
|
||||
report.issues().forEach(issue -> err.println("- " + issue));
|
||||
return 2;
|
||||
}
|
||||
out.println("Ready asset validation passed: " + contentRoot);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static void printUsage(PrintStream stream) {
|
||||
stream.println("Usage:");
|
||||
stream.println(" content-tools validate <content-root>");
|
||||
stream.println(" content-tools digest <content-root>");
|
||||
stream.println(" content-tools ready-assets <content-root> <project-root>");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package dev.projectcapture.contenttools;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Verifies the on-disk deliverables for content which claims to be ready for
|
||||
* shipping. Planned records deliberately remain data-only while artists work.
|
||||
*/
|
||||
public final class ReadyAssetValidator {
|
||||
private static final byte[] PNG_SIGNATURE = new byte[] {
|
||||
(byte) 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a
|
||||
};
|
||||
|
||||
public ValidationReport validate(Path contentRoot, Path projectRoot) {
|
||||
ValidationReport report = new ValidationReport();
|
||||
Path normalizedProjectRoot = projectRoot.toAbsolutePath().normalize();
|
||||
try {
|
||||
ContentBundle bundle = ContentBundle.load(contentRoot);
|
||||
JsonNode assets = bundle.document("assets.json").path("items");
|
||||
for (int index = 0; index < assets.size(); index++) {
|
||||
JsonNode asset = assets.path(index);
|
||||
if (!"READY".equals(asset.path("status").asText())) {
|
||||
continue;
|
||||
}
|
||||
String location = "assets.json.items[" + index + "]";
|
||||
validateJsonArtifact(asset.path("source").asText(), normalizedProjectRoot, location + ".source", report);
|
||||
validateJsonArtifact(asset.path("model").asText(), normalizedProjectRoot, location + ".model", report);
|
||||
validatePng(asset.path("texture").asText(), normalizedProjectRoot, location + ".texture", report);
|
||||
validateJsonArtifact(asset.path("recipe").asText(), normalizedProjectRoot, location + ".recipe", report);
|
||||
JsonNode animations = asset.path("animations");
|
||||
for (String name : List.of("idle", "walk", "attack")) {
|
||||
validateJsonArtifact(animations.path(name).asText(), normalizedProjectRoot,
|
||||
location + ".animations." + name, report);
|
||||
}
|
||||
}
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
report.add("READY_ASSET_CATALOG", "assets.json", exception.getMessage());
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
private void validateJsonArtifact(String declaredPath, Path projectRoot, String location, ValidationReport report) {
|
||||
Path artifact = resolve(declaredPath, projectRoot, location, report);
|
||||
if (artifact == null || !requireNonEmptyFile(artifact, location, report)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (ContentBundle.json().readTree(artifact.toFile()) == null) {
|
||||
report.add("READY_ASSET_JSON", location, "Artifact is empty JSON: " + declaredPath);
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
report.add("READY_ASSET_JSON", location, "Artifact is not valid JSON: " + declaredPath);
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePng(String declaredPath, Path projectRoot, String location, ValidationReport report) {
|
||||
Path artifact = resolve(declaredPath, projectRoot, location, report);
|
||||
if (artifact == null || !requireNonEmptyFile(artifact, location, report)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
byte[] bytes = Files.readAllBytes(artifact);
|
||||
if (bytes.length < PNG_SIGNATURE.length) {
|
||||
report.add("READY_ASSET_PNG", location, "Texture is too small to be a PNG: " + declaredPath);
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < PNG_SIGNATURE.length; index++) {
|
||||
if (bytes[index] != PNG_SIGNATURE[index]) {
|
||||
report.add("READY_ASSET_PNG", location, "Texture has no PNG signature: " + declaredPath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
report.add("READY_ASSET_PNG", location, "Could not inspect texture: " + declaredPath);
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolve(String declaredPath, Path projectRoot, String location, ValidationReport report) {
|
||||
if (declaredPath.isBlank()) {
|
||||
report.add("READY_ASSET_PATH", location, "Ready asset is missing a declared path");
|
||||
return null;
|
||||
}
|
||||
Path artifact = projectRoot.resolve(declaredPath).normalize();
|
||||
if (!artifact.startsWith(projectRoot)) {
|
||||
report.add("READY_ASSET_PATH", location, "Artifact path escapes project root: " + declaredPath);
|
||||
return null;
|
||||
}
|
||||
return artifact;
|
||||
}
|
||||
|
||||
private boolean requireNonEmptyFile(Path artifact, String location, ValidationReport report) {
|
||||
try {
|
||||
if (!Files.isRegularFile(artifact) || Files.size(artifact) == 0) {
|
||||
report.add("READY_ASSET_MISSING", location, "Required generated artifact is missing: " + artifact);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (IOException exception) {
|
||||
report.add("READY_ASSET_MISSING", location, "Could not inspect generated artifact: " + artifact);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,17 @@ class ContentValidatorTest {
|
||||
assertEquals(ContentValidator.UNOFFICIAL_SLICE_SPECIES, ids);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readyAssetRecordsRequireTheirGeneratedFiles() throws IOException {
|
||||
ObjectNode assets = object("assets.json");
|
||||
((ObjectNode) assets.path("items").path(0)).put("status", "READY");
|
||||
write("assets.json", assets);
|
||||
|
||||
ValidationReport report = new ReadyAssetValidator().validate(contentRoot, temporaryDirectory);
|
||||
|
||||
assertTrue(report.containsCode("READY_ASSET_MISSING"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allTwelveSpeciesUseEngineElementRarityAndCaptureSchema() throws IOException {
|
||||
JsonNode species = ContentBundle.load(contentRoot).document("species.json").path("items");
|
||||
|
||||
Reference in New Issue
Block a user