asset details (WIP)

This commit is contained in:
bQUARKz 2026-03-19 15:46:47 +00:00
parent 64df57a774
commit 9c657b0450
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
24 changed files with 694 additions and 215 deletions

View File

@ -86,12 +86,14 @@ Required baseline fields:
Optional baseline field:
- `output.metadata`
- `output.pipeline`
Rules:
- `output.format` defines the semantic/runtime format contract;
- `output.codec` defines storage/extraction behavior;
- `output.metadata` carries runtime-relevant format-specific detail;
- `output.pipeline` carries pipeline-injected metadata kept separate at authoring time;
- codec must remain explicit and must not be hidden inside format naming.
## Metadata Source Segmentation and Runtime Sink
@ -101,6 +103,7 @@ Rules:
Rules:
- declaration-time metadata may come from multiple sources under the asset contract (for example, format metadata, codec-related metadata, and build/pipeline-derived declarations);
- `output.pipeline` may carry nested pipeline-derived metadata objects;
- this segmentation exists for authoring clarity and does not define multiple runtime sinks;
- runtime consumers must read effective metadata from the runtime asset entry metadata sink (`AssetEntry.metadata`);
- convergence/normalization behavior is normative in the build artifact specification.

View File

@ -24,4 +24,6 @@ public interface PackerWorkspaceService {
UpdateAssetContractResponse updateAssetContract(UpdateAssetContractRequest request);
ApplyBankCompositionResponse applyBankComposition(ApplyBankCompositionRequest request);
ApplyPaletteOverhaulingResponse applyPaletteOverhauling(ApplyPaletteOverhaulingRequest request);
}

View File

@ -15,6 +15,7 @@ public record PackerAssetDetailsDTO(
Map<OutputCodecCatalog, List<PackerCodecConfigurationFieldDTO>> codecConfigurationFieldsByCodec,
List<PackerCodecConfigurationFieldDTO> metadataFields,
PackerBankCompositionDetailsDTO bankComposition,
List<Map<String, Object>> pipelinePalettes,
List<PackerDiagnosticDTO> diagnostics) {
public PackerAssetDetailsDTO {
@ -25,6 +26,7 @@ public record PackerAssetDetailsDTO(
codecConfigurationFieldsByCodec = Map.copyOf(Objects.requireNonNull(codecConfigurationFieldsByCodec, "codecConfigurationFieldsByCodec"));
metadataFields = List.copyOf(Objects.requireNonNull(metadataFields, "metadataFields"));
bankComposition = Objects.requireNonNull(bankComposition, "bankComposition");
pipelinePalettes = List.copyOf(Objects.requireNonNull(pipelinePalettes, "pipelinePalettes"));
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
}
}

View File

@ -0,0 +1,20 @@
package p.packer.messages;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public record ApplyPaletteOverhaulingRequest(
PackerProjectContext project,
AssetReference assetReference,
List<Map<String, Object>> selectedPalettes) {
public ApplyPaletteOverhaulingRequest {
Objects.requireNonNull(project, "project");
Objects.requireNonNull(assetReference, "assetReference");
selectedPalettes = List.copyOf(Objects.requireNonNull(selectedPalettes, "selectedPalettes").stream()
.map(palette -> Map.copyOf(new LinkedHashMap<>(Objects.requireNonNull(palette, "palette"))))
.toList());
}
}

View File

@ -0,0 +1,6 @@
package p.packer.messages;
public record ApplyPaletteOverhaulingResponse(
boolean success,
String errorMessage) {
}

View File

@ -1,10 +1,12 @@
package p.packer.models;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.commons.lang3.StringUtils;
import p.packer.messages.assets.AssetFamilyCatalog;
import p.packer.messages.assets.OutputCodecCatalog;
import p.packer.messages.assets.OutputFormatCatalog;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -18,6 +20,7 @@ public record PackerAssetDeclaration(
OutputFormatCatalog outputFormat,
OutputCodecCatalog outputCodec,
Map<String, String> outputMetadata,
Map<String, JsonNode> outputPipelineMetadata,
boolean preloadEnabled) {
public PackerAssetDeclaration {
@ -31,8 +34,20 @@ public record PackerAssetDeclaration(
outputFormat = Objects.requireNonNull(outputFormat, "outputFormat");
outputCodec = Objects.requireNonNull(outputCodec, "outputCodec");
outputMetadata = Map.copyOf(Objects.requireNonNull(outputMetadata, "outputMetadata"));
outputPipelineMetadata = immutablePipelineMetadata(outputPipelineMetadata);
if (StringUtils.isBlank(assetUuid) || StringUtils.isBlank(name)) {
throw new IllegalArgumentException("declaration fields must not be blank");
}
}
private static Map<String, JsonNode> immutablePipelineMetadata(Map<String, JsonNode> pipelineMetadata) {
final Map<String, JsonNode> sanitized = new LinkedHashMap<>();
Objects.requireNonNull(pipelineMetadata, "outputPipelineMetadata").forEach((key, value) -> {
if (key == null || key.isBlank() || value == null) {
return;
}
sanitized.put(key.trim(), value.deepCopy());
});
return Map.copyOf(sanitized);
}
}

View File

@ -15,6 +15,7 @@ public record PackerAssetDetails(
Map<OutputCodecCatalog, List<PackerCodecConfigurationField>> codecConfigurationFieldsByCodec,
List<PackerCodecConfigurationField> metadataFields,
PackerBankCompositionDetails bankComposition,
List<Map<String, Object>> pipelinePalettes,
List<PackerDiagnostic> diagnostics) {
public PackerAssetDetails {
@ -25,6 +26,7 @@ public record PackerAssetDetails(
codecConfigurationFieldsByCodec = Map.copyOf(Objects.requireNonNull(codecConfigurationFieldsByCodec, "codecConfigurationFieldsByCodec"));
metadataFields = List.copyOf(Objects.requireNonNull(metadataFields, "metadataFields"));
bankComposition = Objects.requireNonNull(bankComposition, "bankComposition");
pipelinePalettes = List.copyOf(Objects.requireNonNull(pipelinePalettes, "pipelinePalettes"));
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
}
}

View File

@ -2,6 +2,7 @@ package p.packer.services;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import p.packer.PackerWorkspacePaths;
import p.packer.PackerWorkspaceService;
@ -541,7 +542,8 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
manifest.put("type", request.assetFamily().manifestType());
manifest.put("output", Map.of(
"format", request.outputFormat().manifestValue(),
"codec", request.outputCodec().manifestValue()));
"codec", request.outputCodec().manifestValue(),
"pipeline", Map.of()));
manifest.put("preload", Map.of("enabled", request.preloadEnabled()));
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
}
@ -665,6 +667,13 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
return writeCoordinator.execute(project, () -> applyBankCompositionInWriteLane(safeRequest));
}
@Override
public ApplyPaletteOverhaulingResponse applyPaletteOverhauling(final ApplyPaletteOverhaulingRequest request) {
final ApplyPaletteOverhaulingRequest safeRequest = Objects.requireNonNull(request, "request");
final PackerProjectContext project = safeRequest.project();
return writeCoordinator.execute(project, () -> applyPaletteOverhaulingInWriteLane(safeRequest));
}
private UpdateAssetContractResponse updateAssetContractInWriteLane(UpdateAssetContractRequest request) {
final PackerProjectContext project = request.project();
workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));
@ -772,6 +781,55 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
}
}
private ApplyPaletteOverhaulingResponse applyPaletteOverhaulingInWriteLane(ApplyPaletteOverhaulingRequest request) {
final PackerProjectContext project = request.project();
workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));
final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
final PackerDeleteAssetEvaluation evaluation = actionReadService.evaluateDelete(snapshot, project, request.assetReference());
if (!evaluation.canDelete()) {
return new ApplyPaletteOverhaulingResponse(
false,
Objects.requireNonNullElse(evaluation.reason(), "Asset palette overhauling cannot be updated."));
}
if (request.selectedPalettes().isEmpty()) {
return new ApplyPaletteOverhaulingResponse(false, "At least one palette must be selected.");
}
final Path assetRoot = evaluation.resolved().assetRoot();
final Path manifestPath = assetRoot.resolve("asset.json");
if (!Files.isRegularFile(manifestPath)) {
return new ApplyPaletteOverhaulingResponse(false, "asset.json was not found for the requested asset root.");
}
final ObjectNode manifest;
try {
final JsonNode rawManifest = mapper.readTree(manifestPath.toFile());
if (!(rawManifest instanceof ObjectNode objectNode)) {
return new ApplyPaletteOverhaulingResponse(false, "asset.json must contain a JSON object at the root.");
}
manifest = objectNode;
} catch (IOException exception) {
return new ApplyPaletteOverhaulingResponse(false, "Unable to read asset manifest: " + exception.getMessage());
}
try {
patchManifestPipelinePalettes(manifest, request);
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterUpdateAssetContract(
currentSnapshot,
generation,
assetRoot,
manifestPath,
evaluation.resolved().registryEntry()));
saveRuntimeCache(project, runtime.snapshot());
return new ApplyPaletteOverhaulingResponse(true, null);
} catch (IOException exception) {
return new ApplyPaletteOverhaulingResponse(false, "Unable to update asset palette overhauling: " + exception.getMessage());
} catch (RuntimeException exception) {
return new ApplyPaletteOverhaulingResponse(false, "Unable to update runtime snapshot: " + exception.getMessage());
}
}
private OutputFormatCatalog resolveManifestOutputFormat(ObjectNode manifest) {
final JsonNode outputNode = manifest.get("output");
if (!(outputNode instanceof ObjectNode outputObject)) {
@ -832,6 +890,41 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
}
}
private void patchManifestPipelinePalettes(ObjectNode manifest, ApplyPaletteOverhaulingRequest request) {
final ObjectNode outputNode = mutableObject(manifest, "output");
final ObjectNode pipelineNode = mutableObject(outputNode, "pipeline");
final ArrayNode palettesNode = pipelineNode.putArray("palettes");
for (Map<String, Object> palette : request.selectedPalettes()) {
final List<Integer> originalArgb8888 = integerList(palette.get("originalArgb8888"));
final List<Integer> convertedRgb565 = integerList(palette.get("convertedRgb565"));
if (originalArgb8888.isEmpty() || convertedRgb565.isEmpty()) {
throw new IllegalArgumentException("Each selected palette must contain originalArgb8888 and convertedRgb565 entries.");
}
final ObjectNode paletteNode = palettesNode.addObject();
final ArrayNode originalNode = paletteNode.putArray("originalArgb8888");
originalArgb8888.forEach(originalNode::add);
final ArrayNode convertedNode = paletteNode.putArray("convertedRgb565");
convertedRgb565.forEach(convertedNode::add);
}
if (palettesNode.isEmpty()) {
throw new IllegalArgumentException("At least one palette must be selected.");
}
}
private List<Integer> integerList(Object value) {
if (!(value instanceof Iterable<?> iterable)) {
return List.of();
}
final List<Integer> numbers = new ArrayList<>();
for (Object item : iterable) {
if (!(item instanceof Number number)) {
return List.of();
}
numbers.add(number.intValue());
}
return List.copyOf(numbers);
}
private String contractFingerprint(ObjectNode manifest) {
final JsonNode outputNode = manifest.path("output");
if (!(outputNode instanceof ObjectNode outputObject)) {

View File

@ -50,6 +50,7 @@ public final class PackerAssetDeclarationParser {
final var outputFormat = requiredOutputFormat(root.path("output"), diagnostics, manifestPath);
final var outputCodec = requiredOutputCodec(root.path("output"), diagnostics, manifestPath);
final var outputMetadata = optionalOutputMetadata(root.path("output"), diagnostics, manifestPath);
final var outputPipelineMetadata = optionalOutputPipelineMetadata(root.path("output"), diagnostics, manifestPath);
final var preloadEnabled = requiredBoolean(root.path("preload"), "enabled", diagnostics, manifestPath);
if (schemaVersion != null && !SUPPORTED_SCHEMA_VERSIONS.contains(schemaVersion)) {
@ -75,6 +76,7 @@ public final class PackerAssetDeclarationParser {
outputFormat,
outputCodec,
outputMetadata,
outputPipelineMetadata,
preloadEnabled),
diagnostics);
}
@ -228,6 +230,51 @@ public final class PackerAssetDeclarationParser {
return Map.copyOf(metadata);
}
private Map<String, JsonNode> optionalOutputPipelineMetadata(
final JsonNode node,
final List<PackerDiagnostic> diagnostics,
final Path manifestPath) {
final JsonNode pipelineNode = node.path("pipeline");
if (pipelineNode.isMissingNode() || pipelineNode.isNull()) {
return Map.of();
}
if (!pipelineNode.isObject()) {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.ERROR,
PackerDiagnosticCategory.STRUCTURAL,
"Field 'output.pipeline' must be a JSON object.",
manifestPath,
true));
return Map.of();
}
final Map<String, JsonNode> pipelineMetadata = new LinkedHashMap<>();
pipelineNode.fields().forEachRemaining(entry -> {
final String key = Objects.requireNonNullElse(entry.getKey(), "").trim();
if (key.isBlank()) {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.ERROR,
PackerDiagnosticCategory.STRUCTURAL,
"Field 'output.pipeline' has an invalid empty key.",
manifestPath,
true));
return;
}
final JsonNode valueNode = entry.getValue();
if (valueNode == null) {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.ERROR,
PackerDiagnosticCategory.STRUCTURAL,
"Field 'output.pipeline' values must be valid JSON values.",
manifestPath,
true));
return;
}
pipelineMetadata.put(key, valueNode.deepCopy());
});
return Map.copyOf(pipelineMetadata);
}
private void rejectLegacyInputs(
final JsonNode node,
final List<PackerDiagnostic> diagnostics,

View File

@ -1,5 +1,8 @@
package p.packer.services;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import p.packer.PackerWorkspacePaths;
import p.packer.messages.*;
import p.packer.messages.assets.*;
@ -80,6 +83,7 @@ public final class PackerAssetDetailsService {
outputContract.codecConfigurationFieldsByCodec(),
metadataFields(outputContract.metadataFields(), declaration.outputMetadata()),
resolveBankCompositionDetails(snapshot, runtimeAsset, declaration),
resolvePipelinePalettes(declaration),
diagnostics);
return new GetAssetDetailsResult(
diagnostics.stream().anyMatch(PackerDiagnostic::blocking) ? PackerOperationStatus.PARTIAL : PackerOperationStatus.SUCCESS,
@ -119,6 +123,7 @@ public final class PackerAssetDetailsService {
Map.of(OutputCodecCatalog.NONE, List.of()),
List.of(),
PackerBankCompositionDetails.EMPTY,
List.of(),
diagnostics);
return new GetAssetDetailsResult(
PackerOperationStatus.FAILED,
@ -160,6 +165,49 @@ public final class PackerAssetDetailsService {
.toList();
}
private List<Map<String, Object>> resolvePipelinePalettes(PackerAssetDeclaration declaration) {
final JsonNode palettesNode = declaration.outputPipelineMetadata().get("palettes");
if (!(palettesNode instanceof ArrayNode palettesArray)) {
return List.of();
}
final List<Map<String, Object>> palettes = new ArrayList<>();
for (JsonNode paletteNode : palettesArray) {
final Map<String, Object> normalized = palettePayloadFromNode(paletteNode);
if (!normalized.isEmpty()) {
palettes.add(normalized);
}
}
return List.copyOf(palettes);
}
private Map<String, Object> palettePayloadFromNode(JsonNode paletteNode) {
if (!(paletteNode instanceof ObjectNode paletteObject)) {
return Map.of();
}
final JsonNode originalNode = paletteObject.path("originalArgb8888");
final JsonNode convertedNode = paletteObject.path("convertedRgb565");
if (!originalNode.isArray() || !convertedNode.isArray()) {
return Map.of();
}
final List<Integer> originalArgb8888 = new ArrayList<>();
for (JsonNode colorNode : originalNode) {
if (!colorNode.canConvertToInt()) {
return Map.of();
}
originalArgb8888.add(colorNode.intValue());
}
final List<Integer> convertedRgb565 = new ArrayList<>();
for (JsonNode colorNode : convertedNode) {
if (!colorNode.canConvertToInt()) {
return Map.of();
}
convertedRgb565.add(colorNode.intValue());
}
return Map.of(
"originalArgb8888", List.copyOf(originalArgb8888),
"convertedRgb565", List.copyOf(convertedRgb565));
}
private Optional<PackerBankCompositionFile> resolveSelectedBankFile(
Path assetRoot,
String relativePath,

View File

@ -32,6 +32,7 @@ public final class PackerReadMessageMapper {
toCodecConfigurationFieldsByCodecDTO(details.codecConfigurationFieldsByCodec()),
toCodecConfigurationFieldDTOs(details.metadataFields()),
toBankCompositionDetailsDTO(details.bankComposition()),
details.pipelinePalettes().stream().map(PackerReadMessageMapper::normalizeMetadata).toList(),
toDiagnosticDTOs(details.diagnostics()));
}

View File

@ -1,5 +1,6 @@
package p.packer.repositories;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@ -77,6 +78,7 @@ final class PackerRuntimeAssetMaterializerTest {
OutputFormatCatalog.TILES_INDEXED_V1,
OutputCodecCatalog.NONE,
metadata,
Map.<String, JsonNode>of(),
true);
}

View File

@ -1,6 +1,7 @@
package p.packer.services;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.packer.events.PackerEvent;
@ -106,6 +107,7 @@ final class FileSystemPackerWorkspaceServiceTest {
assertTrue(Files.isRegularFile(projectRoot.resolve("assets/ui/new-atlas/asset.json")));
final String manifestJson = Files.readString(projectRoot.resolve("assets/ui/new-atlas/asset.json"));
assertTrue(manifestJson.contains("\"asset_uuid\""));
assertTrue(manifestJson.contains("\"pipeline\""));
final var snapshot = service.listAssets(new ListAssetsRequest(project(projectRoot)));
assertEquals(1, snapshot.assets().size());
@ -216,6 +218,33 @@ final class FileSystemPackerWorkspaceServiceTest {
assertEquals("mono", manifest.path("output").path("codec_configuration").path("palette").asText());
assertEquals("16x16", manifest.path("output").path("metadata").path("tile_size").asText());
assertEquals("1", manifest.path("output").path("metadata").path("channels").asText());
assertTrue(manifest.path("output").path("pipeline").isMissingNode() || manifest.path("output").path("pipeline").isObject());
}
@Test
void updateAssetContractPreservesPipelineMetadataUnderOutput() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("update-contract-pipeline"));
final Path manifestPath = projectRoot.resolve("assets/ui/atlas/asset.json");
final ObjectNode manifest = (ObjectNode) MAPPER.readTree(manifestPath.toFile());
final ObjectNode pipelineNode = ((ObjectNode) manifest.path("output")).putObject("pipeline");
pipelineNode.putObject("samples").putObject("1").put("offset", 0).put("length", 64);
pipelineNode.put("normalized", true);
MAPPER.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
final FileSystemPackerWorkspaceService service = service();
final var result = service.updateAssetContract(new UpdateAssetContractRequest(
project(projectRoot),
AssetReference.forAssetId(1),
false,
OutputCodecCatalog.NONE,
Map.of("NONE:packMode", "tight"),
Map.of("tile_size", "16x16")));
assertTrue(result.success());
final var updatedManifest = MAPPER.readTree(manifestPath.toFile());
assertTrue(updatedManifest.path("output").path("pipeline").path("normalized").asBoolean());
assertEquals(64, updatedManifest.path("output").path("pipeline").path("samples").path("1").path("length").asInt());
}
@Test
@ -345,6 +374,44 @@ final class FileSystemPackerWorkspaceServiceTest {
assertEquals(1, loader.loadCount());
}
@Test
void applyPaletteOverhaulingWritesPipelinePalettesInOrder() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("apply-palette-overhauling"));
final FileSystemPackerWorkspaceService service = service();
final var response = service.applyPaletteOverhauling(new ApplyPaletteOverhaulingRequest(
project(projectRoot),
AssetReference.forAssetId(1),
List.of(
Map.of(
"originalArgb8888", List.of(0xFFFF0000, 0xFF00FF00),
"convertedRgb565", List.of(0xF800, 0x07E0)),
Map.of(
"originalArgb8888", List.of(0xFF0000FF),
"convertedRgb565", List.of(0x001F)))));
assertTrue(response.success());
final var manifest = MAPPER.readTree(projectRoot.resolve("assets/ui/atlas/asset.json").toFile());
assertEquals(2, manifest.path("output").path("pipeline").path("palettes").size());
assertEquals(0xFFFF0000, manifest.path("output").path("pipeline").path("palettes").get(0).path("originalArgb8888").get(0).asInt());
assertEquals(0x001F, manifest.path("output").path("pipeline").path("palettes").get(1).path("convertedRgb565").get(0).asInt());
}
@Test
void applyPaletteOverhaulingFailsWhenSelectionIsEmpty() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("apply-palette-overhauling-empty"));
final FileSystemPackerWorkspaceService service = service();
final var response = service.applyPaletteOverhauling(new ApplyPaletteOverhaulingRequest(
project(projectRoot),
AssetReference.forAssetId(1),
List.of()));
assertFalse(response.success());
assertTrue(response.errorMessage().contains("At least one palette"));
}
@Test
void deepSyncPreservesContractDrivenBankInvalidationAfterPatchUpdate() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("deep-sync-bank-composition"));

View File

@ -10,6 +10,7 @@ import p.packer.testing.PackerFixtureLocator;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
@ -31,9 +32,65 @@ final class PackerAssetDeclarationParserTest {
assertEquals(AssetFamilyCatalog.TILE_BANK, result.declaration().assetFamily());
assertEquals("TILES/indexed_v1", result.declaration().outputFormat().displayName());
assertEquals(OutputCodecCatalog.NONE, result.declaration().outputCodec());
assertEquals(Map.of(), result.declaration().outputPipelineMetadata());
assertTrue(result.declaration().preloadEnabled());
}
@Test
void parsesPipelineMetadataAsDedicatedOutputObject() throws Exception {
final Path manifest = tempDir.resolve("asset.json");
Files.writeString(manifest, """
{
"schema_version": 1,
"asset_uuid": "uuid-pipeline",
"name": "pipeline_asset",
"type": "sound_bank",
"output": {
"format": "SOUND/v1",
"codec": "NONE",
"pipeline": {
"samples": {
"1": { "offset": 0, "length": 128 }
},
"normalized": true
}
},
"preload": { "enabled": true }
}
""");
final var result = parser.parse(manifest);
assertTrue(result.valid());
assertEquals(2, result.declaration().outputPipelineMetadata().size());
assertTrue(result.declaration().outputPipelineMetadata().get("normalized").asBoolean());
assertEquals(128, result.declaration().outputPipelineMetadata().get("samples").path("1").path("length").asInt());
}
@Test
void rejectsNonObjectPipelineMetadata() throws Exception {
final Path manifest = tempDir.resolve("asset.json");
Files.writeString(manifest, """
{
"schema_version": 1,
"asset_uuid": "uuid-pipeline",
"name": "pipeline_asset",
"type": "tile_bank",
"output": {
"format": "TILES/indexed_v1",
"codec": "NONE",
"pipeline": []
},
"preload": { "enabled": true }
}
""");
final var result = parser.parse(manifest);
assertFalse(result.valid());
assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.message().contains("output.pipeline")));
}
@Test
void rejectsMalformedJsonWithStructuralDiagnostic() {
final var result = parser.parse(PackerFixtureLocator.fixtureRoot("workspaces/invalid-malformed/assets/bad/asset.json"));

View File

@ -49,10 +49,35 @@ final class PackerAssetDetailsServiceTest {
assertEquals(List.of(), result.details().codecConfigurationFieldsByCodec().get(OutputCodecCatalog.NONE));
assertNotNull(result.details().bankComposition());
assertTrue(result.details().bankComposition().selectedFiles().isEmpty());
assertTrue(result.details().pipelinePalettes().isEmpty());
assertTrue(result.diagnostics().stream().noneMatch(diagnostic -> diagnostic.blocking()));
assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.message().contains("Output metadata for tile bank cannot be empty")));
}
@Test
void exposesPipelinePalettesFromOutputPipelineMetadata() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("managed-pipeline-palettes"));
final Path manifestPath = projectRoot.resolve("assets/ui/atlas/asset.json");
final ObjectMapper mapper = new ObjectMapper();
final ObjectNode manifest = (ObjectNode) mapper.readTree(manifestPath.toFile());
final ObjectNode pipeline = ((ObjectNode) manifest.path("output")).putObject("pipeline");
final var palettes = pipeline.putArray("palettes");
palettes.addObject()
.putArray("originalArgb8888").add(0xFFFF0000).add(0xFF00FF00);
((ObjectNode) palettes.get(0))
.putArray("convertedRgb565").add(0xF800).add(0x07E0);
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
final PackerAssetDetailsService service = service();
final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), AssetReference.forAssetId(1)));
assertEquals(1, result.details().pipelinePalettes().size());
assertEquals(
List.of(0xFFFF0000, 0xFF00FF00),
result.details().pipelinePalettes().getFirst().get("originalArgb8888"));
}
@Test
void projectsBankCompositionAvailableAndSelectedFiles() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("managed-bank-composition"));

View File

@ -203,7 +203,7 @@ public abstract class StudioDualListView<T> extends HBox {
return;
}
final String text = indexed
? (getIndex() + 1) + ". " + itemText(item)
? getIndex() + ". " + itemText(item)
: itemText(item);
setText(text);
setGraphic(null);
@ -259,7 +259,7 @@ public abstract class StudioDualListView<T> extends HBox {
}
private Node createIndexedGraphic(Node graphic, int index) {
final Label chip = new Label((index + 1) + ".");
final Label chip = new Label(index + ".");
chip.getStyleClass().add("studio-dual-list-index-chip");
final HBox wrapper = new HBox(8, chip, graphic);
wrapper.getStyleClass().add("studio-dual-list-indexed-cell");

View File

@ -73,7 +73,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
this.summaryControl = new AssetDetailsSummaryControl(projectReference, workspaceBus);
this.contractControl = new AssetDetailsContractControl(projectReference, workspaceBus);
this.bankCompositionControl = new AssetDetailsBankCompositionControl(projectReference, workspaceBus);
this.paletteOverhaulingControl = new AssetDetailsPaletteOverhaulingControl(workspaceBus);
this.paletteOverhaulingControl = new AssetDetailsPaletteOverhaulingControl(projectReference, workspaceBus);
this.actionsSection = createActionsSection();
getStyleClass().add("assets-workspace-pane");
@ -556,6 +556,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
details.codecConfigurationFieldsByCodec(),
details.metadataFields(),
mapBankComposition(details.bankComposition()),
details.pipelinePalettes(),
mergedDiagnostics);
}

View File

@ -1,23 +1,33 @@
package p.studio.workspaces.assets.details.palette;
import javafx.application.Platform;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import p.packer.events.PackerEventKind;
import p.packer.messages.ApplyPaletteOverhaulingRequest;
import p.studio.Container;
import p.studio.controls.forms.StudioFormEditScopeChangedEvent;
import p.studio.controls.forms.StudioFormMode;
import p.studio.controls.forms.StudioFormSection;
import p.studio.events.StudioPackerOperationEvent;
import p.studio.events.StudioWorkspaceEventBus;
import p.studio.projects.ProjectReference;
import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
import p.studio.workspaces.assets.messages.events.StudioAssetLogEvent;
import p.studio.workspaces.assets.messages.events.StudioAssetsRefreshRequestedEvent;
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
import p.studio.workspaces.framework.StudioSubscriptionBag;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSection {
private static final String SECTION_ID = "asset-details.palette-overhauling";
private final ProjectReference projectReference;
private final StudioWorkspaceEventBus workspaceBus;
private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag();
private final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
@ -28,7 +38,8 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
private AssetWorkspaceDetailsViewState viewState;
public AssetDetailsPaletteOverhaulingControl(StudioWorkspaceEventBus workspaceBus) {
public AssetDetailsPaletteOverhaulingControl(ProjectReference projectReference, StudioWorkspaceEventBus workspaceBus) {
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus");
body.getStyleClass().add("assets-details-palette-overhauling-body");
content.setFillWidth(true);
@ -149,9 +160,49 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
@Override
protected void apply() {
coordinator.apply();
publishEditScope(workspaceBus, currentScopeKey(), null);
renderSection();
if (viewState == null || viewState.selectedAssetReference() == null || !coordinator.ready()) {
return;
}
final var selectedPalettes = coordinator.viewModel().selectedFiles().stream()
.map(file -> palettePayload(file.metadata()))
.filter(Objects::nonNull)
.toList();
if (selectedPalettes.isEmpty()) {
workspaceBus.publish(new StudioAssetLogEvent("palette-overhauling", "Apply failed: no palette was selected."));
Container.eventBus().publish(new StudioPackerOperationEvent(
UUID.randomUUID().toString(),
PackerEventKind.ACTION_FAILED,
"Palette overhauling apply failed: no palette was selected.",
null,
true));
return;
}
final var assetReference = viewState.selectedAssetReference();
Container.backgroundTasks().submit(() -> {
final var request = new ApplyPaletteOverhaulingRequest(
projectReference.toPackerProjectContext(),
assetReference,
selectedPalettes);
try {
final var response = Container.packer().workspaceService().applyPaletteOverhauling(request);
Platform.runLater(() -> {
if (response.success()) {
coordinator.apply();
publishEditScope(workspaceBus, currentScopeKey(), null);
renderSection();
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(assetReference));
} else {
workspaceBus.publish(new StudioAssetLogEvent("palette-overhauling",
"Apply failed: " + Objects.requireNonNullElse(response.errorMessage(), "unknown error")));
}
});
} catch (Exception exception) {
Platform.runLater(() -> workspaceBus.publish(new StudioAssetLogEvent(
"palette-overhauling",
"Apply failed: " + Objects.requireNonNullElse(exception.getMessage(), "unknown error"))));
}
});
}
@Override
@ -180,4 +231,10 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
? null
: "asset-details:" + viewState.selectedAssetReference();
}
@SuppressWarnings("unchecked")
private static Map<String, Object> palettePayload(Map<String, Object> metadata) {
final Object palette = metadata.get("palette");
return palette instanceof Map<?, ?> paletteMap ? (Map<String, Object>) paletteMap : null;
}
}

View File

@ -27,13 +27,7 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
final List<AssetWorkspaceBankCompositionFile> previewTileFiles = details.bankComposition().selectedFiles().stream()
.filter(AssetDetailsPaletteOverhaulingCoordinator::supportsPalettePreview)
.toList();
final Map<String, AssetWorkspaceBankCompositionFile> filesByPath = allFiles.stream()
.collect(java.util.stream.Collectors.toMap(
AssetWorkspaceBankCompositionFile::path,
file -> file,
(left, right) -> left,
LinkedHashMap::new));
final List<AssetWorkspaceBankCompositionFile> initialSelected = new ArrayList<>();
final List<AssetWorkspaceBankCompositionFile> initialSelected = initialSelectedPaletteFiles(details, allFiles);
final List<AssetWorkspaceBankCompositionFile> available = new ArrayList<>(allFiles);
available.removeAll(initialSelected);
@ -169,6 +163,29 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
metadata);
}
private static List<AssetWorkspaceBankCompositionFile> initialSelectedPaletteFiles(
AssetWorkspaceAssetDetails details,
List<AssetWorkspaceBankCompositionFile> allFiles) {
final List<AssetWorkspaceBankCompositionFile> selected = new ArrayList<>();
for (Map<String, Object> palette : details.pipelinePalettes()) {
for (AssetWorkspaceBankCompositionFile file : allFiles) {
if (!selected.contains(file) && paletteEquals(palette, nestedMap(file.metadata(), "palette"))) {
selected.add(file);
break;
}
}
}
return List.copyOf(selected);
}
private static boolean paletteEquals(Map<String, Object> left, Map<String, Object> right) {
if (left == null || right == null) {
return false;
}
return Objects.equals(left.get("originalArgb8888"), right.get("originalArgb8888"))
&& Objects.equals(left.get("convertedRgb565"), right.get("convertedRgb565"));
}
@SuppressWarnings("unchecked")
private static String paletteHash(AssetWorkspaceBankCompositionFile file) {
final Map<String, Object> palette = nestedMap(file.metadata(), "palette");

View File

@ -18,6 +18,7 @@ public record AssetWorkspaceAssetDetails(
Map<OutputCodecCatalog, List<PackerCodecConfigurationFieldDTO>> codecConfigurationFieldsByCodec,
List<PackerCodecConfigurationFieldDTO> metadataFields,
AssetWorkspaceBankCompositionDetails bankComposition,
List<Map<String, Object>> pipelinePalettes,
List<PackerDiagnosticDTO> diagnostics) {
public AssetWorkspaceAssetDetails {
@ -29,6 +30,7 @@ public record AssetWorkspaceAssetDetails(
codecConfigurationFieldsByCodec = Map.copyOf(Objects.requireNonNull(codecConfigurationFieldsByCodec, "codecConfigurationFieldsByCodec"));
metadataFields = List.copyOf(Objects.requireNonNull(metadataFields, "metadataFields"));
bankComposition = Objects.requireNonNull(bankComposition, "bankComposition");
pipelinePalettes = List.copyOf(Objects.requireNonNull(pipelinePalettes, "pipelinePalettes"));
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
}
}

View File

@ -112,6 +112,7 @@ final class AssetDetailsBankCompositionCoordinatorTest {
files("tile", fileCount, 1024L),
List.of(),
0L),
List.of(),
List.of());
}
@ -130,6 +131,7 @@ final class AssetDetailsBankCompositionCoordinatorTest {
new AssetWorkspaceBankCompositionFile("b.wav", "b.wav", secondSize, 1L, null, Map.of())),
List.of(),
0L),
List.of(),
List.of());
}

View File

@ -91,6 +91,7 @@ final class AssetDetailsPaletteOverhaulingCoordinatorTest {
Map.of(OutputCodecCatalog.NONE, List.of()),
List.of(),
new AssetWorkspaceBankCompositionDetails(availableFiles, selectedFiles, 0L),
selectedFiles.stream().map(file -> (Map<String, Object>) file.metadata().get("palette")).toList(),
List.of());
}

View File

@ -198,6 +198,206 @@
"message" : "Asset scan started",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "7 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bbb2",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: Bigode",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan started",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "7 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bbb2",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: Bigode",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan started",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "7 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bbb2",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: Bigode",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan started",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "7 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bbb2",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: Bigode",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan started",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Excluded asset in build: ui/sound",
@ -2298,204 +2498,4 @@
"message" : "Included asset in build: bigode",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "7 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bbb2",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: Bigode",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan started",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Excluded asset in build: bigode",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "7 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bbb2",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: Bigode",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan started",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Included asset in build: bigode",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "7 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bbb2",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: Bigode",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan started",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Excluded asset in build: bigode",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "7 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bbb2",
"severity" : "INFO",
"sticky" : false
} ]

View File

@ -9,6 +9,15 @@
"codec_configuration" : { },
"metadata" : {
"tile_size" : "32x32"
},
"pipeline" : {
"palettes" : [ {
"originalArgb8888" : [ -265674, -1736296, -13905598, -11518505, -14439577, -2467509 ],
"convertedRgb565" : [ 65414, 58387, 11912, 20986, 9548, 56009 ]
}, {
"originalArgb8888" : [ -265674 ],
"convertedRgb565" : [ 65414 ]
} ]
}
},
"preload" : {