diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerRuntimeMaterializationConfig.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerRuntimeMaterializationConfig.java new file mode 100644 index 00000000..5443cf25 --- /dev/null +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerRuntimeMaterializationConfig.java @@ -0,0 +1,22 @@ +package p.packer.models; + +import java.util.Objects; +import java.util.function.Predicate; + +public record PackerRuntimeMaterializationConfig( + PackerWalkMode mode, + Predicate projectionFilter) { + + public PackerRuntimeMaterializationConfig { + mode = Objects.requireNonNull(mode, "mode"); + projectionFilter = Objects.requireNonNull(projectionFilter, "projectionFilter"); + } + + public static PackerRuntimeMaterializationConfig runtimeDefault() { + return new PackerRuntimeMaterializationConfig(PackerWalkMode.RUNTIME, probe -> true); + } + + public static PackerRuntimeMaterializationConfig packingBuild() { + return new PackerRuntimeMaterializationConfig(PackerWalkMode.PACKING, probe -> true); + } +} diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerRuntimeWalkFile.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerRuntimeWalkFile.java index e9eede0e..2d032ef8 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerRuntimeWalkFile.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerRuntimeWalkFile.java @@ -4,6 +4,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; public record PackerRuntimeWalkFile( String relativePath, @@ -11,6 +12,7 @@ public record PackerRuntimeWalkFile( long size, long lastModified, String fingerprint, + Optional contentBytes, Map metadata, List diagnostics) { @@ -24,6 +26,8 @@ public record PackerRuntimeWalkFile( throw new IllegalArgumentException("lastModified must be non-negative"); } fingerprint = fingerprint == null || fingerprint.isBlank() ? null : fingerprint; + final Optional safeContentBytes = Objects.requireNonNull(contentBytes, "contentBytes"); + contentBytes = safeContentBytes.map(byte[]::clone); metadata = Map.copyOf(new LinkedHashMap<>(Objects.requireNonNull(metadata, "metadata"))); diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics")); } diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerWalkMode.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerWalkMode.java new file mode 100644 index 00000000..dd47fd9a --- /dev/null +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerWalkMode.java @@ -0,0 +1,6 @@ +package p.packer.models; + +public enum PackerWalkMode { + RUNTIME, + PACKING +} diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/repositories/PackerRuntimeAssetMaterializer.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/repositories/PackerRuntimeAssetMaterializer.java index f82e91e7..1019afa3 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/repositories/PackerRuntimeAssetMaterializer.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/repositories/PackerRuntimeAssetMaterializer.java @@ -20,11 +20,28 @@ public final class PackerRuntimeAssetMaterializer { Optional registryEntry, PackerAssetDeclarationParseResult parseResult, Optional priorAssetCache) { + return materialize( + assetRoot, + manifestPath, + registryEntry, + parseResult, + priorAssetCache, + PackerRuntimeMaterializationConfig.runtimeDefault()); + } + + public PackerRuntimeAssetMaterialization materialize( + Path assetRoot, + Path manifestPath, + Optional registryEntry, + PackerAssetDeclarationParseResult parseResult, + Optional priorAssetCache, + PackerRuntimeMaterializationConfig materializationConfig) { final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize(); final Path normalizedManifestPath = Objects.requireNonNull(manifestPath, "manifestPath").toAbsolutePath().normalize(); final Optional safeRegistryEntry = Objects.requireNonNull(registryEntry, "registryEntry"); final PackerAssetDeclarationParseResult safeParseResult = Objects.requireNonNull(parseResult, "parseResult"); final Optional safePriorAssetCache = Objects.requireNonNull(priorAssetCache, "priorAssetCache"); + final PackerRuntimeMaterializationConfig safeMaterializationConfig = Objects.requireNonNull(materializationConfig, "materializationConfig"); final String currentContractFingerprint = safeParseResult.valid() ? PackerContractFingerprint.metadataFingerprint(safeParseResult.declaration().outputMetadata()) : null; @@ -47,7 +64,8 @@ public final class PackerRuntimeAssetMaterializer { safeParseResult.declaration(), reusablePriorAssetCache(safePriorAssetCache, currentContractFingerprint)); final List buildCandidateFiles = walkResult.probeResults().stream() - .map(probeResult -> toRuntimeWalkFile(normalizedAssetRoot, probeResult)) + .filter(probeResult -> shouldIncludeProbe(safeParseResult.declaration(), normalizedAssetRoot, probeResult, safeMaterializationConfig)) + .map(probeResult -> toRuntimeWalkFile(normalizedAssetRoot, probeResult, safeMaterializationConfig)) .sorted(Comparator.comparing(PackerRuntimeWalkFile::relativePath, String.CASE_INSENSITIVE_ORDER)) .toList(); final long measuredBankSizeBytes = buildCandidateFiles.stream() @@ -105,18 +123,47 @@ public final class PackerRuntimeAssetMaterializer { } } - private PackerRuntimeWalkFile toRuntimeWalkFile(Path assetRoot, PackerProbeResult probeResult) { + private PackerRuntimeWalkFile toRuntimeWalkFile( + Path assetRoot, + PackerProbeResult probeResult, + PackerRuntimeMaterializationConfig materializationConfig) { final PackerFileProbe fileProbe = probeResult.fileProbe(); + final String relativePath = assetRoot.relativize(fileProbe.path().toAbsolutePath().normalize()).toString().replace('\\', '/'); return new PackerRuntimeWalkFile( - assetRoot.relativize(fileProbe.path().toAbsolutePath().normalize()).toString().replace('\\', '/'), + relativePath, fileProbe.mimeType(), fileProbe.size(), fileProbe.lastModified(), PackerFileFingerprint.sha256(fileProbe), + materializationConfig.mode() == PackerWalkMode.PACKING + ? Optional.of(fileProbe.content()) + : Optional.empty(), probeResult.metadata(), probeResult.diagnostics()); } + private boolean shouldIncludeProbe( + PackerAssetDeclaration declaration, + Path assetRoot, + PackerProbeResult probeResult, + PackerRuntimeMaterializationConfig materializationConfig) { + if (!materializationConfig.projectionFilter().test(probeResult)) { + return false; + } + if (materializationConfig.mode() != PackerWalkMode.PACKING) { + return true; + } + if (declaration.artifacts().isEmpty()) { + return false; + } + final String relativePath = assetRoot.relativize(probeResult.fileProbe().path().toAbsolutePath().normalize()) + .toString() + .replace('\\', '/'); + return declaration.artifacts().stream() + .map(PackerAssetArtifactSelection::file) + .anyMatch(relativePath::equals); + } + public record PackerRuntimeAssetMaterialization( PackerRuntimeAsset runtimeAsset, Optional assetCacheEntry) { diff --git a/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/repositories/PackerRuntimeAssetMaterializerTest.java b/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/repositories/PackerRuntimeAssetMaterializerTest.java index d9d906e8..1547a101 100644 --- a/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/repositories/PackerRuntimeAssetMaterializerTest.java +++ b/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/repositories/PackerRuntimeAssetMaterializerTest.java @@ -9,6 +9,8 @@ import p.packer.messages.assets.OutputCodecCatalog; import p.packer.messages.assets.OutputFormatCatalog; import p.packer.models.*; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -68,20 +70,102 @@ final class PackerRuntimeAssetMaterializerTest { assertTrue(walker.lastPriorAssetCache().isEmpty()); } + @Test + void runtimeDefaultProjectionSuppressesContentBytes() throws Exception { + final var materializer = new PackerRuntimeAssetMaterializer(new PackerAssetWalker(MAPPER)); + final Path assetRoot = Files.createDirectories(tempDir.resolve("runtime-default")); + final Path manifestPath = assetRoot.resolve("asset.json"); + Files.writeString(manifestPath, "{}"); + writeTile(assetRoot.resolve("selected.png"), 16); + writeTile(assetRoot.resolve("extra.png"), 16); + + final PackerAssetDeclaration declaration = declaration( + Map.of("tile_size", "16x16"), + List.of(new PackerAssetArtifactSelection("selected.png", 0)), + palettePipeline()); + final var materialized = materializer.materialize( + assetRoot, + manifestPath, + Optional.of(new PackerRegistryEntry(1, "uuid", "asset", true)), + new PackerAssetDeclarationParseResult(declaration, List.of()), + Optional.empty()); + + assertEquals(2, materialized.runtimeAsset().walkProjection().buildCandidateFiles().size()); + assertTrue(materialized.runtimeAsset().walkProjection().buildCandidateFiles().stream() + .allMatch(file -> file.contentBytes().isEmpty())); + } + + @Test + void packingProjectionKeepsSelectedFilesAndInjectsContentBytes() throws Exception { + final var materializer = new PackerRuntimeAssetMaterializer(new PackerAssetWalker(MAPPER)); + final Path assetRoot = Files.createDirectories(tempDir.resolve("packing")); + final Path manifestPath = assetRoot.resolve("asset.json"); + Files.writeString(manifestPath, "{}"); + writeTile(assetRoot.resolve("selected.png"), 16); + writeTile(assetRoot.resolve("extra.png"), 16); + + final PackerAssetDeclaration declaration = declaration( + Map.of("tile_size", "16x16"), + List.of(new PackerAssetArtifactSelection("selected.png", 0)), + palettePipeline()); + final var materialized = materializer.materialize( + assetRoot, + manifestPath, + Optional.of(new PackerRegistryEntry(1, "uuid", "asset", true)), + new PackerAssetDeclarationParseResult(declaration, List.of()), + Optional.empty(), + PackerRuntimeMaterializationConfig.packingBuild()); + + assertEquals(1, materialized.runtimeAsset().walkProjection().buildCandidateFiles().size()); + final var file = materialized.runtimeAsset().walkProjection().buildCandidateFiles().getFirst(); + assertEquals("selected.png", file.relativePath()); + assertTrue(file.contentBytes().isPresent()); + assertTrue(file.contentBytes().get().length > 0); + } + private static PackerAssetDeclaration declaration(Map metadata) { + return declaration(metadata, List.of(), Map.of()); + } + + private static PackerAssetDeclaration declaration( + Map metadata, + List artifacts, + Map pipelineMetadata) { return new PackerAssetDeclaration( 1, "uuid", "asset", AssetFamilyCatalog.TILE_BANK, - List.of(), + artifacts, OutputFormatCatalog.TILES_INDEXED_V1, OutputCodecCatalog.NONE, metadata, - Map.of(), + pipelineMetadata, true); } + private static Map palettePipeline() { + final var payload = MAPPER.createObjectNode(); + payload.putArray("originalArgb8888").add(0xFFFF0000); + payload.putArray("convertedRgb565").add(0xF800); + final var declaration = MAPPER.createObjectNode(); + declaration.put("index", 0); + declaration.set("palette", payload); + final var palettes = MAPPER.createArrayNode(); + palettes.add(declaration); + return Map.of("palettes", palettes); + } + + private static void writeTile(Path path, int tileSize) throws Exception { + 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, 0xFFFF0000); + } + } + ImageIO.write(image, "png", path.toFile()); + } + private static final class TrackingWalker extends PackerAssetWalker { private Optional lastPriorAssetCache = Optional.empty();