implements PR-35

This commit is contained in:
bQUARKz 2026-03-20 09:44:12 +00:00
parent 99d1070c46
commit fa527720a4
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
5 changed files with 168 additions and 5 deletions

View File

@ -0,0 +1,22 @@
package p.packer.models;
import java.util.Objects;
import java.util.function.Predicate;
public record PackerRuntimeMaterializationConfig(
PackerWalkMode mode,
Predicate<PackerProbeResult> 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);
}
}

View File

@ -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<byte[]> contentBytes,
Map<String, Object> metadata,
List<PackerDiagnostic> 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<byte[]> 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"));
}

View File

@ -0,0 +1,6 @@
package p.packer.models;
public enum PackerWalkMode {
RUNTIME,
PACKING
}

View File

@ -20,11 +20,28 @@ public final class PackerRuntimeAssetMaterializer {
Optional<PackerRegistryEntry> registryEntry,
PackerAssetDeclarationParseResult parseResult,
Optional<PackerAssetCacheEntry> priorAssetCache) {
return materialize(
assetRoot,
manifestPath,
registryEntry,
parseResult,
priorAssetCache,
PackerRuntimeMaterializationConfig.runtimeDefault());
}
public PackerRuntimeAssetMaterialization materialize(
Path assetRoot,
Path manifestPath,
Optional<PackerRegistryEntry> registryEntry,
PackerAssetDeclarationParseResult parseResult,
Optional<PackerAssetCacheEntry> priorAssetCache,
PackerRuntimeMaterializationConfig materializationConfig) {
final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
final Path normalizedManifestPath = Objects.requireNonNull(manifestPath, "manifestPath").toAbsolutePath().normalize();
final Optional<PackerRegistryEntry> safeRegistryEntry = Objects.requireNonNull(registryEntry, "registryEntry");
final PackerAssetDeclarationParseResult safeParseResult = Objects.requireNonNull(parseResult, "parseResult");
final Optional<PackerAssetCacheEntry> 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<PackerRuntimeWalkFile> 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<PackerAssetCacheEntry> assetCacheEntry) {

View File

@ -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<String, String> metadata) {
return declaration(metadata, List.of(), Map.of());
}
private static PackerAssetDeclaration declaration(
Map<String, String> metadata,
List<PackerAssetArtifactSelection> artifacts,
Map<String, JsonNode> pipelineMetadata) {
return new PackerAssetDeclaration(
1,
"uuid",
"asset",
AssetFamilyCatalog.TILE_BANK,
List.of(),
artifacts,
OutputFormatCatalog.TILES_INDEXED_V1,
OutputCodecCatalog.NONE,
metadata,
Map.<String, JsonNode>of(),
pipelineMetadata,
true);
}
private static Map<String, JsonNode> 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<PackerAssetCacheEntry> lastPriorAssetCache = Optional.empty();