implements PR-34
This commit is contained in:
parent
37b898a9bb
commit
99d1070c46
@ -1,5 +1,6 @@
|
||||
package p.packer.repositories;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.commons.collections4.MapUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@ -16,6 +17,8 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public class PackerAssetWalker {
|
||||
private static final int TILE_BANK_EMITTED_SHEET_SIZE = 256;
|
||||
private static final int TILE_BANK_MAX_PALETTES = 64;
|
||||
private static final Set<OutputFormatCatalog> TILE_BANK_SUPPORTED_FORMATS = Set.of(
|
||||
OutputFormatCatalog.TILES_INDEXED_V1
|
||||
);
|
||||
@ -65,8 +68,11 @@ public class PackerAssetWalker {
|
||||
true));
|
||||
return new PackerWalkResult(List.of(), diagnostics);
|
||||
}
|
||||
diagnostics.addAll(tileBankDeclarationDiagnostics(assetRoot, declaration, requirementBuildResult.requirements));
|
||||
final var walkResult = tileBankWalker.walk(assetRoot, requirementBuildResult.requirements, priorAssetCache);
|
||||
diagnostics.addAll(walkResult.diagnostics());
|
||||
diagnostics.addAll(tileBankSelectedArtifactDiagnostics(assetRoot, declaration, walkResult));
|
||||
diagnostics.addAll(tileBankFragileIndexDiagnostics(declaration, walkResult));
|
||||
return new PackerWalkResult(walkResult.probeResults(), diagnostics);
|
||||
}
|
||||
case SOUND_BANK -> {
|
||||
@ -135,6 +141,157 @@ public class PackerAssetWalker {
|
||||
return RequirementBuildResult.success(new PackerTileBankRequirements(tileSize));
|
||||
}
|
||||
|
||||
private List<PackerDiagnostic> tileBankDeclarationDiagnostics(
|
||||
final Path assetRoot,
|
||||
final PackerAssetDeclaration declaration,
|
||||
final PackerTileBankRequirements requirements) {
|
||||
final List<PackerDiagnostic> diagnostics = new ArrayList<>();
|
||||
final List<PackerAssetArtifactSelection> artifacts = declaration.artifacts();
|
||||
final Set<Integer> seenIndices = new java.util.HashSet<>();
|
||||
for (int expectedIndex = 0; expectedIndex < artifacts.size(); expectedIndex += 1) {
|
||||
final PackerAssetArtifactSelection artifact = artifacts.get(expectedIndex);
|
||||
if (!seenIndices.add(artifact.index())) {
|
||||
diagnostics.add(new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.ERROR,
|
||||
PackerDiagnosticCategory.STRUCTURAL,
|
||||
"Tile bank artifacts contain duplicate index " + artifact.index() + ".",
|
||||
assetRoot,
|
||||
true));
|
||||
}
|
||||
if (artifact.index() != expectedIndex) {
|
||||
diagnostics.add(new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.ERROR,
|
||||
PackerDiagnosticCategory.STRUCTURAL,
|
||||
"Tile bank artifacts must form a contiguous index range starting at 0.",
|
||||
assetRoot,
|
||||
true));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final int tileCapacity = (TILE_BANK_EMITTED_SHEET_SIZE / requirements.tileSize())
|
||||
* (TILE_BANK_EMITTED_SHEET_SIZE / requirements.tileSize());
|
||||
if (artifacts.size() > tileCapacity) {
|
||||
diagnostics.add(new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.ERROR,
|
||||
PackerDiagnosticCategory.STRUCTURAL,
|
||||
"Tile bank exceeds emitted sheet capacity for tile_size "
|
||||
+ requirements.tileSize() + "x" + requirements.tileSize()
|
||||
+ ": capacity is " + tileCapacity + " tiles.",
|
||||
assetRoot,
|
||||
true));
|
||||
}
|
||||
|
||||
final List<DeclaredPalette> declaredPalettes = declaredPalettes(declaration);
|
||||
if (!artifacts.isEmpty() && declaredPalettes.isEmpty()) {
|
||||
diagnostics.add(new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.ERROR,
|
||||
PackerDiagnosticCategory.STRUCTURAL,
|
||||
"Tile bank must declare at least one palette in output.pipeline.palettes.",
|
||||
assetRoot,
|
||||
true));
|
||||
}
|
||||
if (declaredPalettes.size() > TILE_BANK_MAX_PALETTES) {
|
||||
diagnostics.add(new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.ERROR,
|
||||
PackerDiagnosticCategory.STRUCTURAL,
|
||||
"Tile bank cannot declare more than " + TILE_BANK_MAX_PALETTES + " palettes.",
|
||||
assetRoot,
|
||||
true));
|
||||
}
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
private List<PackerDiagnostic> tileBankSelectedArtifactDiagnostics(
|
||||
final Path assetRoot,
|
||||
final PackerAssetDeclaration declaration,
|
||||
final PackerWalkResult walkResult) {
|
||||
final List<PackerDiagnostic> diagnostics = new ArrayList<>();
|
||||
final Set<String> discoveredPaths = walkResult.probeResults().stream()
|
||||
.map(result -> assetRoot.toAbsolutePath().normalize()
|
||||
.relativize(result.fileProbe().path().toAbsolutePath().normalize())
|
||||
.toString()
|
||||
.replace('\\', '/'))
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
declaration.artifacts().stream()
|
||||
.map(PackerAssetArtifactSelection::file)
|
||||
.filter(path -> !discoveredPaths.contains(path))
|
||||
.forEach(path -> diagnostics.add(new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.ERROR,
|
||||
PackerDiagnosticCategory.STRUCTURAL,
|
||||
"Selected tile artifact is missing or unsupported: " + path,
|
||||
assetRoot.resolve(path),
|
||||
true)));
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
private List<PackerDiagnostic> tileBankFragileIndexDiagnostics(
|
||||
final PackerAssetDeclaration declaration,
|
||||
final PackerWalkResult walkResult) {
|
||||
final List<DeclaredPalette> declaredPalettes = declaredPalettes(declaration);
|
||||
if (declaredPalettes.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
final List<PackerDiagnostic> diagnostics = new ArrayList<>();
|
||||
for (PackerProbeResult probeResult : walkResult.probeResults()) {
|
||||
if (probeResult.diagnostics().stream().anyMatch(PackerDiagnostic::blocking)) {
|
||||
continue;
|
||||
}
|
||||
final Object tileValue = probeResult.metadata().get("tile");
|
||||
if (!(tileValue instanceof PackerTileIndexedV1 tile)) {
|
||||
continue;
|
||||
}
|
||||
int maxUsedIndex = 0;
|
||||
for (byte value : tile.paletteIndices()) {
|
||||
maxUsedIndex = Math.max(maxUsedIndex, Byte.toUnsignedInt(value));
|
||||
}
|
||||
if (maxUsedIndex == 0) {
|
||||
continue;
|
||||
}
|
||||
final int requiredColorCount = maxUsedIndex;
|
||||
final Optional<DeclaredPalette> fragilePalette = declaredPalettes.stream()
|
||||
.filter(palette -> palette.colorCount() < requiredColorCount)
|
||||
.findFirst();
|
||||
if (fragilePalette.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
diagnostics.add(new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.WARNING,
|
||||
PackerDiagnosticCategory.HYGIENE,
|
||||
"Tile uses fragile palette indices up to " + requiredColorCount
|
||||
+ ", but declared palette " + fragilePalette.get().index()
|
||||
+ " only covers " + fragilePalette.get().colorCount() + " colors.",
|
||||
probeResult.fileProbe().path(),
|
||||
false));
|
||||
}
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
private List<DeclaredPalette> declaredPalettes(final PackerAssetDeclaration declaration) {
|
||||
final JsonNode palettesNode = declaration.outputPipelineMetadata().get("palettes");
|
||||
if (!(palettesNode instanceof com.fasterxml.jackson.databind.node.ArrayNode palettesArray)) {
|
||||
return List.of();
|
||||
}
|
||||
final List<DeclaredPalette> palettes = new ArrayList<>();
|
||||
for (JsonNode paletteDeclaration : palettesArray) {
|
||||
final JsonNode indexNode = paletteDeclaration.path("index");
|
||||
final JsonNode paletteNode = paletteDeclaration.path("palette");
|
||||
if (!indexNode.isInt() || !paletteNode.isObject()) {
|
||||
continue;
|
||||
}
|
||||
final int originalCount = paletteNode.path("originalArgb8888").isArray()
|
||||
? paletteNode.path("originalArgb8888").size()
|
||||
: 0;
|
||||
final int convertedCount = paletteNode.path("convertedRgb565").isArray()
|
||||
? paletteNode.path("convertedRgb565").size()
|
||||
: 0;
|
||||
palettes.add(new DeclaredPalette(indexNode.intValue(), Math.min(originalCount, convertedCount)));
|
||||
}
|
||||
palettes.sort(java.util.Comparator.comparingInt(DeclaredPalette::index));
|
||||
return List.copyOf(palettes);
|
||||
}
|
||||
|
||||
private RequirementBuildResult<PackerSoundBankRequirements> buildSoundBankRequirements(
|
||||
final PackerAssetDeclaration declaration,
|
||||
final Map<String, String> metadata) {
|
||||
@ -169,4 +326,9 @@ public class PackerAssetWalker {
|
||||
return StringUtils.isNotBlank(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private record DeclaredPalette(
|
||||
int index,
|
||||
int colorCount) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -703,6 +703,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
final List<PackerDiagnostic> blockingDiagnostics = Stream.of(
|
||||
parsed.diagnostics(),
|
||||
runtimeAsset.walkDiagnostics(),
|
||||
collectWalkFileDiagnostics(runtimeAsset),
|
||||
identityMismatchDiagnostics(registryEntry, parsed, runtimeAsset.manifestPath()))
|
||||
.flatMap(Collection::stream)
|
||||
.filter(PackerDiagnostic::blocking)
|
||||
@ -715,6 +716,14 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
PackerReadMessageMapper.toDiagnosticDTOs(blockingDiagnostics));
|
||||
}
|
||||
|
||||
private List<PackerDiagnostic> collectWalkFileDiagnostics(PackerRuntimeAsset runtimeAsset) {
|
||||
final List<PackerDiagnostic> diagnostics = new ArrayList<>();
|
||||
for (PackerRuntimeWalkFile file : runtimeAsset.walkProjection().buildCandidateFiles()) {
|
||||
diagnostics.addAll(file.diagnostics());
|
||||
}
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
private void saveRuntimeCache(PackerProjectContext project, PackerRuntimeSnapshot snapshot) {
|
||||
cacheRepository.save(project, snapshot.cacheState());
|
||||
}
|
||||
|
||||
@ -198,6 +198,91 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
assertTrue(result.assets().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void packValidationBlocksTileBanksWithoutDeclaredPalettes() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("pack-validation-no-palettes"));
|
||||
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
|
||||
final Path manifestPath = assetRoot.resolve("asset.json");
|
||||
writeTilePng(assetRoot.resolve("confirm.png"), 16);
|
||||
|
||||
final ObjectNode manifest = (ObjectNode) MAPPER.readTree(manifestPath.toFile());
|
||||
manifest.putObject("output").put("format", "TILES/indexed_v1").put("codec", "NONE");
|
||||
final var artifacts = manifest.putArray("artifacts");
|
||||
artifacts.addObject().put("file", "confirm.png").put("index", 0);
|
||||
MAPPER.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
|
||||
|
||||
final FileSystemPackerWorkspaceService service = service();
|
||||
final var result = service.validatePackWorkspace(new ValidatePackWorkspaceRequest(project(projectRoot)));
|
||||
|
||||
assertEquals(PackerOperationStatus.PARTIAL, result.status());
|
||||
assertFalse(result.canPack());
|
||||
assertTrue(result.assets().getFirst().diagnostics().stream()
|
||||
.anyMatch(diagnostic -> diagnostic.message().contains("must declare at least one palette")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void packValidationIncludesBlockingFileScopedTileDiagnostics() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("pack-validation-file-diagnostics"));
|
||||
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
|
||||
final Path manifestPath = assetRoot.resolve("asset.json");
|
||||
writeTilePng(assetRoot.resolve("oversized.png"), 32);
|
||||
|
||||
final ObjectNode manifest = (ObjectNode) MAPPER.readTree(manifestPath.toFile());
|
||||
final ObjectNode output = (ObjectNode) manifest.path("output");
|
||||
output.putObject("metadata").put("tile_size", "16x16");
|
||||
final ObjectNode pipeline = output.putObject("pipeline");
|
||||
final var palettes = pipeline.putArray("palettes");
|
||||
final ObjectNode palette = palettes.addObject();
|
||||
palette.put("index", 0);
|
||||
palette.putObject("palette")
|
||||
.putArray("originalArgb8888").add(0xFFFF0000);
|
||||
((ObjectNode) palette.path("palette"))
|
||||
.putArray("convertedRgb565").add(0xF800);
|
||||
final var artifacts = manifest.putArray("artifacts");
|
||||
artifacts.addObject().put("file", "oversized.png").put("index", 0);
|
||||
MAPPER.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
|
||||
|
||||
final FileSystemPackerWorkspaceService service = service();
|
||||
final var result = service.validatePackWorkspace(new ValidatePackWorkspaceRequest(project(projectRoot)));
|
||||
|
||||
assertEquals(PackerOperationStatus.PARTIAL, result.status());
|
||||
assertFalse(result.canPack());
|
||||
assertTrue(result.assets().getFirst().diagnostics().stream()
|
||||
.anyMatch(diagnostic -> diagnostic.message().contains("Invalid tile dimensions for oversized.png")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fragileTileIndicesRemainWarningOnlyForValidation() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("pack-validation-fragile-indices"));
|
||||
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
|
||||
final Path manifestPath = assetRoot.resolve("asset.json");
|
||||
writeTwoColorTilePng(assetRoot.resolve("checker.png"), 16);
|
||||
|
||||
final ObjectNode manifest = (ObjectNode) MAPPER.readTree(manifestPath.toFile());
|
||||
final ObjectNode output = (ObjectNode) manifest.path("output");
|
||||
output.putObject("metadata").put("tile_size", "16x16");
|
||||
final ObjectNode pipeline = output.putObject("pipeline");
|
||||
final var palettes = pipeline.putArray("palettes");
|
||||
final ObjectNode palette = palettes.addObject();
|
||||
palette.put("index", 0);
|
||||
palette.putObject("palette")
|
||||
.putArray("originalArgb8888").add(0xFFFF0000);
|
||||
((ObjectNode) palette.path("palette"))
|
||||
.putArray("convertedRgb565").add(0xF800);
|
||||
final var artifacts = manifest.putArray("artifacts");
|
||||
artifacts.addObject().put("file", "checker.png").put("index", 0);
|
||||
MAPPER.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
|
||||
|
||||
final FileSystemPackerWorkspaceService service = service();
|
||||
final var details = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), AssetReference.forAssetId(1)));
|
||||
final var validation = service.validatePackWorkspace(new ValidatePackWorkspaceRequest(project(projectRoot)));
|
||||
|
||||
assertEquals(PackerOperationStatus.SUCCESS, validation.status());
|
||||
assertTrue(validation.canPack());
|
||||
assertTrue(details.diagnostics().stream()
|
||||
.anyMatch(diagnostic -> diagnostic.message().contains("fragile palette indices")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsRegisteredAssetAndWritesManifest() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("created");
|
||||
@ -478,7 +563,9 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
final var detailsResult = service.getAssetDetails(new GetAssetDetailsRequest(
|
||||
project(projectRoot),
|
||||
AssetReference.forAssetId(1)));
|
||||
assertEquals(PackerOperationStatus.SUCCESS, detailsResult.status());
|
||||
assertEquals(PackerOperationStatus.PARTIAL, detailsResult.status());
|
||||
assertTrue(detailsResult.diagnostics().stream()
|
||||
.anyMatch(diagnostic -> diagnostic.message().contains("must declare at least one palette")));
|
||||
assertEquals(
|
||||
List.of("cancel.png", "confirm.png"),
|
||||
detailsResult.details().bankComposition().selectedFiles().stream().map(file -> file.path()).toList());
|
||||
@ -1074,4 +1161,15 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
}
|
||||
ImageIO.write(image, "png", path.toFile());
|
||||
}
|
||||
|
||||
private void writeTwoColorTilePng(Path path, int tileSize) throws Exception {
|
||||
Files.createDirectories(path.getParent());
|
||||
final BufferedImage image = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < tileSize; y += 1) {
|
||||
for (int x = 0; x < tileSize; x += 1) {
|
||||
image.setRGB(x, y, (x + y) % 2 == 0 ? 0xFFFF0000 : 0xFF00FF00);
|
||||
}
|
||||
}
|
||||
ImageIO.write(image, "png", path.toFile());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user