asset walker (WIP)

This commit is contained in:
bQUARKz 2026-03-18 18:53:17 +00:00
parent 5b2e2b023f
commit 92c74c7bd0
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
43 changed files with 1565 additions and 726 deletions

View File

@ -4,7 +4,7 @@ This directory contains active packer discussion agendas.
## Active Agendas
There are currently no active packer agendas.
1. [`Tilemap and Metatile Runtime Binary Layout Agenda.md`](./Tilemap%20and%20Metatile%20Runtime%20Binary%20Layout%20Agenda.md)
The first packer agenda wave was consolidated into normative specs under [`../specs/`](../specs/) and didactic material under [`../learn/`](../learn/).

View File

@ -0,0 +1,152 @@
# Tilemap and Metatile Runtime Binary Layout Agenda
## Status
Open
## Purpose
Convergir a discussão sobre o contrato de mapa para handheld em duas camadas:
- formato de autoria/edição (JSON legível),
- formato de execução (layout binário compacto em runtime).
O objetivo é fechar qual estrutura será adotada para `tilemap bank`, `tileset bank` e dados de colisão/flags, com foco em previsibilidade de memória e streaming de mapas.
## Domain Owner
`docs/packer`
Este tema é owner de packer porque envolve transformação de artefatos (`JSON -> BIN`), contrato de build e layout de saída para consumo do runtime.
Domínios impactados (cross-domain):
- `docs/vm-arch` (contrato de leitura em runtime e limites de memória),
- `docs/studio` (edição/preview de mapas no workspace),
- `docs/compiler/pbs` (integração futura com referências de assets em código, quando aplicável).
## Problem
Hoje existe alinhamento conceitual de que:
1. visual e colisão devem ser separados por responsabilidade;
2. o mapa não deve repetir metadados extensos por célula;
3. apenas uma janela ativa de até `9` mapas deve ficar residente em memória.
Mas ainda não existe decisão formal sobre:
- layout binário final por célula em runtime,
- orçamento por mapa e por janela ativa,
- responsabilidade exata entre `map bank` e `tileset bank`.
Sem esse contrato, o packer não fecha especificação de saída e o runtime/studio ficam sem baseline único.
## Context
Premissas atuais da discussão:
- `tileset bank` pode ter tamanho diferente dos demais banks;
- `map bank` não precisa seguir o mesmo tamanho de `tileset bank`;
- mapa deve referenciar IDs compactos (visual, colisão e flags), em vez de duplicar estrutura completa por célula;
- paletas por bank continuam sendo opção válida para preservar decisões artísticas locais;
- orçamento alvo foi discutido no contexto de `64 KiB` por map bank e janela ativa de `3x3` mapas (`9` residentes).
## Options
### Option A — Célula `u8` (mapa ultra-compacto)
- Cada célula armazena apenas `metatileId` (`0..255`).
- Colisão/flags vêm de tabela auxiliar por `metatileId`.
### Option B — Célula `u16` bit-packed (recomendação inicial)
- Cada célula usa `16 bits` com divisão sugerida:
- `visualId`: `10 bits` (`0..1023`),
- `collisionId`: `5 bits` (`0..31`),
- `flags`: `1 bit` (`0..1`) ou reservado para evolução.
- Permite desacoplamento visual/lógico sem custo de `u32`.
### Option C — Célula `u32` (maior flexibilidade)
- Exemplo de divisão:
- `visualId`: `12 bits`,
- `collisionId`: `8 bits`,
- `flags/event`: `12 bits`.
- Ganho de expressividade para triggers/eventos inline; custo de memória dobra vs `u16`.
## Tradeoffs
- Option A minimiza RAM e I/O, mas limita variedade visual e desloca muita semântica para tabelas externas.
- Option B oferece bom equilíbrio para handheld: compacta, previsível e com espaço suficiente para muitos casos de mapa.
- Option C simplifica evolução funcional (eventos por célula), mas pressiona memória e banda de streaming sem necessidade comprovada agora.
## Runtime Binary Structure (focus)
Estrutura sugerida para runtime (baseada em Option B):
1. **Map Header (fixo)**
- `magic` (`4 bytes`)
- `version` (`u16`)
- `width` (`u16`)
- `height` (`u16`)
- `cellEncoding` (`u8`) — ex.: `1 = U16_PACKED_V1`
- `visualBankId` (`u16`)
- `collisionBankId` (`u16`)
- `reserved` / `checksum` (conforme decisão posterior)
2. **Cell Stream**
- vetor contínuo com `width * height` células `u16`, little-endian;
- leitura linear favorece cache e descompressão simples.
3. **Optional Chunks (future-proof)**
- bloco opcional de `eventTriggers`;
- bloco opcional de `spawnPoints`;
- bloco opcional de `navHints`.
Packing de célula (`U16_PACKED_V1`):
- `bits 0..9` => `visualId`
- `bits 10..14` => `collisionId`
- `bit 15` => `flag0`
Decodificação de referência:
- `visualId -> metatile visual table -> 4 subtiles (8x8) + palette/flip/priority`
- `collisionId -> collision/material table -> walk/solid/swim/damage/etc.`
## Memory Notes for Active Window (`9` maps)
Assumindo `64 KiB` por map bank:
- `1` mapa residente: `64 KiB`
- `9` mapas residentes: `576 KiB`
Capacidade por encoding dentro de `64 KiB`:
- `u8`: `65,536` células (`256x256` máximo quadrado)
- `u16`: `32,768` células (`~181x181` máximo quadrado, ou retângulos equivalentes)
- `u32`: `16,384` células (`128x128` máximo quadrado)
## Recommendation
Adotar `Option B` como baseline para decisão:
1. autoria em JSON orientada a IDs (`visualId`, `collisionId`, `flags`);
2. empacotamento determinístico para `U16_PACKED_V1` no build;
3. janela ativa de runtime limitada a `9` mapas com budget explícito;
4. extensão para `u32` somente via nova versão de encoding e evidência de necessidade.
## Open Questions
1. `collisionId` com `5 bits` (`32` classes) é suficiente para os biomas/projetos previstos?
2. `flag0` deve ser reservado para trigger rápido ou para variação visual contextual?
3. Quais chunks opcionais entram já em `V1` e quais ficam para `V2`?
4. O `map bank` seguirá estritamente `64 KiB` ou terá tamanho variável com metadado de capacidade?
5. Qual política de compressão do stream (`none`, `LZ4`, etc.) será padrão no packer?
## Expected Follow-up
1. Abrir `decision` em `docs/packer/decisions` fechando o encoding `U16_PACKED_V1`.
2. Propagar contrato de leitura para `docs/vm-arch`.
3. Definir no `docs/studio/specs` o schema de edição JSON correspondente.
4. Planejar PR de implementação (`packer` + `runtime`) com testes de roundtrip (`JSON -> BIN -> decode`).

View File

@ -5,4 +5,5 @@ plugins {
dependencies {
implementation(project(":prometeu-infra"))
implementation(project(":prometeu-packer:prometeu-packer-api"))
implementation("org.apache.tika:tika-core:3.2.1")
}

View File

@ -2,6 +2,9 @@ package p.packer;
import com.fasterxml.jackson.databind.ObjectMapper;
import p.packer.events.PackerEventSink;
import p.packer.repositories.PackerAssetWalker;
import p.packer.repositories.PackerRuntimeLoader;
import p.packer.repositories.PackerRuntimeRegistry;
import p.packer.services.*;
import java.io.Closeable;
@ -23,9 +26,11 @@ public final class Packer implements Closeable {
public static Packer bootstrap(final ObjectMapper mapper, final PackerEventSink eventSink) {
final var resolvedEventSink = Objects.requireNonNull(eventSink, "eventSink");
final var workspaceFoundation = new PackerWorkspaceFoundation();
final var workspaceFoundation = new PackerWorkspaceFoundation(mapper);
final var declarationParser = new PackerAssetDeclarationParser(mapper);
final var runtimeRegistry = new PackerRuntimeRegistry(new PackerRuntimeLoader(workspaceFoundation, declarationParser));
final var assetWalker = new PackerAssetWalker(mapper);
final var runtimeLoader = new PackerRuntimeLoader(workspaceFoundation, declarationParser, assetWalker);
final var runtimeRegistry = new PackerRuntimeRegistry(runtimeLoader);
final var assetReferenceResolver = new PackerAssetReferenceResolver(workspaceFoundation.lookup());
final var assetDetailsService = new PackerAssetDetailsService(runtimeRegistry, assetReferenceResolver);
final var assetActionReadService = new PackerAssetActionReadService(

View File

@ -1,4 +1,4 @@
package p.packer.services;
package p.packer;
import p.packer.messages.PackerProjectContext;
@ -9,6 +9,7 @@ public final class PackerWorkspacePaths {
private static final String ASSETS_DIR = "assets";
private static final String PROMETEU_DIR = ".prometeu";
private static final String REGISTRY_FILE = "index.json";
private static final String CACHE_FILE = "cache.json";
private PackerWorkspacePaths() {
}
@ -25,6 +26,10 @@ public final class PackerWorkspacePaths {
return registryDirectory(project).resolve(REGISTRY_FILE).toAbsolutePath().normalize();
}
public static Path cachePath(PackerProjectContext project) {
return registryDirectory(project).resolve(CACHE_FILE).toAbsolutePath().normalize();
}
public static Path assetRoot(PackerProjectContext project, String relativeRoot) {
return assetsRoot(project).resolve(relativeRoot).toAbsolutePath().normalize();
}

View File

@ -0,0 +1,10 @@
package p.packer.models;
import java.nio.file.Path;
public record PackerFileProbe(
Path path,
String mimeType,
long lastModified,
byte[] content) {
}

View File

@ -0,0 +1,6 @@
package p.packer.models;
public record PackerPaletteV1(
int[] originalArgb8888,
short[] convertedRgb565) {
}

View File

@ -0,0 +1,10 @@
package p.packer.models;
import java.util.List;
import java.util.Map;
public record PackerProbeResult(
PackerFileProbe fileProbe,
Map<String, Object> metadata,
List<PackerDiagnostic> diagnostics) {
}

View File

@ -0,0 +1,4 @@
package p.packer.models;
public record PackerSoundBankRequirements(int sampleRate, int channels) {
}

View File

@ -0,0 +1,4 @@
package p.packer.models;
public record PackerTileBankRequirements(int tileSize) {
}

View File

@ -0,0 +1,7 @@
package p.packer.models;
public record PackerTileIndexedV1(
int width,
int height,
byte[] paletteIndices) {
}

View File

@ -0,0 +1,16 @@
package p.packer.models;
import java.util.List;
import java.util.Objects;
public record PackerWalkResult(
List<PackerProbeResult> probeResults,
List<PackerDiagnostic> diagnostics) {
public static final PackerWalkResult EMPTY = new PackerWalkResult(List.of(), List.of());
public PackerWalkResult {
probeResults = List.copyOf(Objects.requireNonNull(probeResults, "probeResults"));
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
}
}

View File

@ -1,4 +1,4 @@
package p.packer.services;
package p.packer.repositories;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@ -7,6 +7,7 @@ import p.packer.exceptions.PackerRegistryException;
import p.packer.messages.PackerProjectContext;
import p.packer.models.PackerRegistryEntry;
import p.packer.models.PackerRegistryState;
import p.packer.PackerWorkspacePaths;
import java.io.IOException;
import java.nio.file.Files;
@ -14,9 +15,14 @@ import java.nio.file.Path;
import java.util.*;
public final class FileSystemPackerRegistryRepository implements PackerRegistryRepository {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final int REGISTRY_SCHEMA_VERSION = 1;
private final ObjectMapper mapper;
public FileSystemPackerRegistryRepository(final ObjectMapper mapper) {
this.mapper = Objects.requireNonNull(mapper, "mapper");
}
@Override
public PackerRegistryState load(PackerProjectContext project) {
final Path registryPath = PackerWorkspacePaths.registryPath(project);
@ -25,10 +31,10 @@ public final class FileSystemPackerRegistryRepository implements PackerRegistryR
}
try {
final RegistryDocument document = MAPPER.readValue(registryPath.toFile(), RegistryDocument.class);
final RegistryDocument document = mapper.readValue(registryPath.toFile(), RegistryDocument.class);
final int schemaVersion = document.schemaVersion <= 0 ? REGISTRY_SCHEMA_VERSION : document.schemaVersion;
if (schemaVersion != REGISTRY_SCHEMA_VERSION) {
throw new p.packer.exceptions.PackerRegistryException("Unsupported registry schema_version: " + schemaVersion);
throw new PackerRegistryException("Unsupported registry schema_version: " + schemaVersion);
}
final List<PackerRegistryEntry> entries = new ArrayList<>();
if (document.assets != null) {
@ -73,7 +79,7 @@ public final class FileSystemPackerRegistryRepository implements PackerRegistryR
entry.root(),
entry.includedInBuild()))
.toList();
MAPPER.writerWithDefaultPrettyPrinter().writeValue(registryPath.toFile(), document);
mapper.writerWithDefaultPrettyPrinter().writeValue(registryPath.toFile(), document);
} catch (IOException exception) {
throw new p.packer.exceptions.PackerRegistryException("Unable to save registry: " + registryPath, exception);
}

View File

@ -0,0 +1,92 @@
package p.packer.repositories;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.tika.Tika;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.PackerDiagnostic;
import p.packer.models.PackerFileProbe;
import p.packer.models.PackerProbeResult;
import p.packer.models.PackerWalkResult;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
public abstract class PackerAbstractBankWalker<R> {
private final Tika tika = new Tika();
protected final ObjectMapper mapper;
protected PackerAbstractBankWalker(ObjectMapper mapper) {
this.mapper = mapper;
}
protected abstract Set<String> getSupportedMimeTypes();
protected abstract PackerProbeResult processFileProbe(PackerFileProbe fileProbe, R requirements);
private Optional<PackerFileProbe> getFileProbeIfSupported(
final Path filePath,
final Set<String> supportedMimeTypes) throws IOException {
final var bytes = Files.readAllBytes(filePath);
final var mimeType = tika.detect(bytes);
if (!supportedMimeTypes.contains(mimeType)) {
return Optional.empty();
}
final var lastModified = filePath.toFile().lastModified();
final var fileProbe = new PackerFileProbe(filePath, mimeType, lastModified, bytes);
return Optional.of(fileProbe);
}
private List<Path> retrieveValidPaths(final Path assetRoot) throws IOException {
try (final var paths = Files.list(assetRoot)
.filter(Files::isRegularFile)
.filter(path -> !isAssetManifest(path))) {
return paths.toList();
}
}
private boolean isAssetManifest(final Path path) {
return path.getFileName().toString().equalsIgnoreCase("asset.json");
}
private List<PackerFileProbe> processFileProbeWhenSupported(
final Path assetRoot,
final Set<String> supportedMimeTypes,
final List<PackerDiagnostic> diagnostics) {
final List<PackerFileProbe> supportedFileProbes = new ArrayList<>();
try {
final var validPaths = retrieveValidPaths(assetRoot);
for (final var filePath : validPaths) {
final var fileProbeMaybe = getFileProbeIfSupported(filePath, supportedMimeTypes);
if (fileProbeMaybe.isEmpty()) {
continue;
}
final var fileProbe = fileProbeMaybe.get();
supportedFileProbes.add(fileProbe);
}
} catch (IOException exception) {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.ERROR,
PackerDiagnosticCategory.STRUCTURAL,
"Unable to walk tile asset root: " + exception.getMessage(),
assetRoot,
true
));
}
return supportedFileProbes;
}
public PackerWalkResult walk(
final Path assetRoot,
final R requirements) {
final List<PackerDiagnostic> rootDiagnostics = new ArrayList<>();
final List<PackerProbeResult> probeResults = new ArrayList<>();
processFileProbeWhenSupported(assetRoot, getSupportedMimeTypes(), rootDiagnostics)
.forEach(fileProbe -> probeResults.add(processFileProbe(fileProbe, requirements)));
return new PackerWalkResult(probeResults, rootDiagnostics);
}
}

View File

@ -0,0 +1,164 @@
package p.packer.repositories;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import p.packer.messages.assets.OutputFormatCatalog;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.*;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class PackerAssetWalker {
private static final Set<OutputFormatCatalog> TILE_BANK_SUPPORTED_FORMATS = Set.of(
OutputFormatCatalog.TILES_INDEXED_V1
);
private static final Set<OutputFormatCatalog> SOUND_BANK_SUPPORTED_FORMATS = Set.of(
OutputFormatCatalog.SOUND_V1
);
private final PackerTileBankWalker tileBankWalker;
private final PackerSoundBankWalker soundBankWalker;
public PackerAssetWalker(final ObjectMapper mapper) {
this.tileBankWalker = new PackerTileBankWalker(mapper);
this.soundBankWalker = new PackerSoundBankWalker(mapper);
}
public PackerWalkResult walk(
final Path assetRoot,
final PackerAssetDeclaration declaration) {
final List<PackerDiagnostic> diagnostics = new ArrayList<>();
switch (declaration.assetFamily()) {
case TILE_BANK -> {
var metadata = declaration.outputMetadata();
if (MapUtils.isEmpty(metadata)) {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.WARNING,
PackerDiagnosticCategory.HYGIENE,
"Output metadata for tile bank cannot be empty, using default values",
assetRoot,
false));
metadata = Map.of("tile_size", "16x16");
}
final var requirementBuildResult = buildTileBankRequirements(declaration, metadata);
if (requirementBuildResult.hasError()) {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.ERROR,
PackerDiagnosticCategory.STRUCTURAL,
requirementBuildResult.errorMessage(),
assetRoot,
true));
return new PackerWalkResult(List.of(), diagnostics);
}
final var walkResult = tileBankWalker.walk(assetRoot, requirementBuildResult.requirements);
diagnostics.addAll(walkResult.diagnostics());
return new PackerWalkResult(walkResult.probeResults(), diagnostics);
}
case SOUND_BANK -> {
var metadata = declaration.outputMetadata();
if (MapUtils.isEmpty(metadata)) {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.WARNING,
PackerDiagnosticCategory.HYGIENE,
"Output metadata for tile bank cannot be empty, using default values",
assetRoot,
false));
metadata = Map.of("sample_rate", "44100", "channels", "1");
}
final var result = buildSoundBankRequirements(declaration, metadata);
if (result.hasError()) {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.ERROR,
PackerDiagnosticCategory.STRUCTURAL,
result.errorMessage(),
assetRoot,
true));
return new PackerWalkResult(List.of(), diagnostics);
}
final var walkResult = soundBankWalker.walk(assetRoot, result.requirements);
diagnostics.addAll(walkResult.diagnostics());
return new PackerWalkResult(walkResult.probeResults(), diagnostics);
}
case UNKNOWN -> {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.WARNING,
PackerDiagnosticCategory.STRUCTURAL,
"Unknown asset family for declaration, skipping content walk",
assetRoot,
false));
return new PackerWalkResult(List.of(), diagnostics);
}
}
return PackerWalkResult.EMPTY;
}
private RequirementBuildResult<PackerTileBankRequirements> buildTileBankRequirements(
final PackerAssetDeclaration declaration,
final Map<String, String> metadata) {
if (!TILE_BANK_SUPPORTED_FORMATS.contains(declaration.outputFormat())) {
return RequirementBuildResult.fail("Unsupported output format for tile bank: " + declaration.outputFormat());
}
final var tileSizeStr = metadata.get("tile_size");
if (StringUtils.isBlank(tileSizeStr)) {
return RequirementBuildResult.fail("Tile size metadata for tile bank cannot be empty");
}
final int tileSize;
switch (tileSizeStr) {
case "8x8": {
tileSize = 8;
} break;
case "16x16": {
tileSize = 16;
} break;
case "32x32": {
tileSize = 32;
} break;
default: {
return RequirementBuildResult.fail("Unsupported tile size for tile bank: " + tileSizeStr);
}
}
return RequirementBuildResult.success(new PackerTileBankRequirements(tileSize));
}
private RequirementBuildResult<PackerSoundBankRequirements> buildSoundBankRequirements(
final PackerAssetDeclaration declaration,
final Map<String, String> metadata) {
if (!SOUND_BANK_SUPPORTED_FORMATS.contains(declaration.outputFormat())) {
return RequirementBuildResult.fail("Unsupported output format for sound bank: " + declaration.outputFormat());
}
final var sampleRateStr = metadata.get("sample_rate");
if (StringUtils.isBlank(sampleRateStr)) {
return RequirementBuildResult.fail("Missing sample rate for sound bank");
}
final var sampleRate = Integer.parseInt(sampleRateStr);
final var channelsStr = metadata.get("");
if (StringUtils.isBlank(channelsStr)) {
return RequirementBuildResult.fail("Missing channels for sound bank");
}
final var channels = Integer.parseInt(channelsStr);
return RequirementBuildResult.success(new PackerSoundBankRequirements(sampleRate, channels));
}
private record RequirementBuildResult<T>(
T requirements,
String errorMessage) {
static <T> RequirementBuildResult<T> success(T requirements) {
return new RequirementBuildResult<>(requirements, null);
}
static <T> RequirementBuildResult<T> fail(String errorMessage) {
return new RequirementBuildResult<>(null, errorMessage);
}
public boolean hasError() {
return StringUtils.isNotBlank(errorMessage);
}
}
}

View File

@ -1,4 +1,4 @@
package p.packer.services;
package p.packer.repositories;
import p.packer.messages.PackerProjectContext;
import p.packer.models.PackerRegistryState;

View File

@ -1,11 +1,12 @@
package p.packer.services;
package p.packer.repositories;
import p.packer.messages.InitWorkspaceRequest;
import p.packer.messages.PackerProjectContext;
import p.packer.models.PackerRegistryEntry;
import p.packer.models.PackerRegistryState;
import p.packer.models.PackerRuntimeAsset;
import p.packer.models.PackerRuntimeSnapshot;
import p.packer.services.PackerAssetDeclarationParser;
import p.packer.services.PackerWorkspaceFoundation;
import p.packer.PackerWorkspacePaths;
import java.io.IOException;
import java.nio.file.Files;
@ -17,12 +18,15 @@ import java.util.stream.Collectors;
public final class PackerRuntimeLoader implements PackerRuntimeSnapshotLoader {
private final PackerWorkspaceFoundation workspaceFoundation;
private final PackerAssetDeclarationParser parser;
private final PackerAssetWalker assetWalker;
public PackerRuntimeLoader(
PackerWorkspaceFoundation workspaceFoundation,
PackerAssetDeclarationParser parser) {
final PackerWorkspaceFoundation workspaceFoundation,
final PackerAssetDeclarationParser parser,
final PackerAssetWalker assetWalker) {
this.workspaceFoundation = Objects.requireNonNull(workspaceFoundation, "workspaceFoundation");
this.parser = Objects.requireNonNull(parser, "parser");
this.assetWalker = Objects.requireNonNull(assetWalker, "assetWalker");
}
private boolean isAssetJson(Path path, BasicFileAttributes attrs) {
@ -31,11 +35,13 @@ public final class PackerRuntimeLoader implements PackerRuntimeSnapshotLoader {
@Override
public PackerRuntimeSnapshot load(PackerProjectContext project, long generation) {
final PackerProjectContext safeProject = Objects.requireNonNull(project, "project");
final var safeProject = Objects.requireNonNull(project, "project");
workspaceFoundation.initWorkspace(new InitWorkspaceRequest(safeProject));
final PackerRegistryState registry = workspaceFoundation.loadRegistry(safeProject);
final Map<Path, PackerRegistryEntry> registryByRoot = registry.assets().stream()
final var registry = workspaceFoundation.loadRegistry(safeProject);
final var registryByRoot = registry
.assets()
.stream()
.collect(Collectors.toMap(
entry -> PackerWorkspacePaths.assetRoot(safeProject, entry.root()),
entry -> entry));
@ -51,11 +57,13 @@ public final class PackerRuntimeLoader implements PackerRuntimeSnapshotLoader {
for (final var manifestPath : manifests) {
final var assetRoot = manifestPath.getParent();
final var registryEntry = Optional.ofNullable(registryByRoot.get(assetRoot));
assets.add(new PackerRuntimeAsset(
assetRoot,
manifestPath,
registryEntry,
parser.parse(manifestPath)));
final var parseResult = parser.parse(manifestPath);
if (parseResult.valid()) {
final var walkResult = assetWalker.walk(assetRoot, parseResult.declaration());
}
final var runtimeAsset = new PackerRuntimeAsset(assetRoot, manifestPath, registryEntry, parseResult);
assets.add(runtimeAsset);
}
} catch (IOException exception) {
throw new p.packer.exceptions.PackerRegistryException(

View File

@ -1,7 +1,8 @@
package p.packer.services;
package p.packer.repositories;
import p.packer.messages.PackerProjectContext;
import p.packer.models.PackerRuntimeSnapshot;
import p.packer.services.PackerProjectRuntime;
import java.nio.file.Path;
import java.util.Objects;

View File

@ -1,4 +1,4 @@
package p.packer.services;
package p.packer.repositories;
import p.packer.messages.PackerProjectContext;
import p.packer.models.PackerRuntimeSnapshot;

View File

@ -0,0 +1,33 @@
package p.packer.repositories;
import com.fasterxml.jackson.databind.ObjectMapper;
import p.packer.models.PackerDiagnostic;
import p.packer.models.PackerFileProbe;
import p.packer.models.PackerProbeResult;
import p.packer.models.PackerSoundBankRequirements;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class PackerSoundBankWalker extends PackerAbstractBankWalker<PackerSoundBankRequirements> {
private static final Set<String> SUPPORTED_MIME_TYPES = Set.of("audio/wav");
public PackerSoundBankWalker(ObjectMapper mapper) {
super(mapper);
}
@Override
protected Set<String> getSupportedMimeTypes() {
return SUPPORTED_MIME_TYPES;
}
@Override
protected PackerProbeResult processFileProbe(
final PackerFileProbe fileProbe,
final PackerSoundBankRequirements requirements) {
final List<PackerDiagnostic> diagnostics = new ArrayList<>();
return new PackerProbeResult(fileProbe, Map.of(), diagnostics);
}
}

View File

@ -0,0 +1,231 @@
package p.packer.repositories;
import com.fasterxml.jackson.databind.ObjectMapper;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.*;
import javax.imageio.ImageIO;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
public class PackerTileBankWalker extends PackerAbstractBankWalker<PackerTileBankRequirements> {
private static final int MAX_COLORS_PER_PALETTE = 15;
private static final int COLOR_KEY_RGB = 0x00FF00FF;
private static final Set<String> SUPPORTED_MIME_TYPES = Set.of(
"image/png"
);
public PackerTileBankWalker(ObjectMapper mapper) {
super(mapper);
}
@Override
protected Set<String> getSupportedMimeTypes() {
return SUPPORTED_MIME_TYPES;
}
@Override
protected PackerProbeResult processFileProbe(
final PackerFileProbe fileProbe,
final PackerTileBankRequirements requirements) {
final List<PackerDiagnostic> diagnostics = new ArrayList<>();
final var image = readImage(fileProbe, diagnostics);
if (image == null) {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.ERROR,
PackerDiagnosticCategory.STRUCTURAL,
"Unable to decode tile image content: " + fileProbe.path().getFileName(),
fileProbe.path(),
true
));
return new PackerProbeResult(fileProbe, Map.of(), diagnostics);
}
final var width = image.getWidth();
final var height = image.getHeight();
if (width != requirements.tileSize() || height != requirements.tileSize()) {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.ERROR,
PackerDiagnosticCategory.STRUCTURAL,
"Invalid tile dimensions for " + fileProbe.path().getFileName()
+ ": expected " + requirements.tileSize() + "x" + requirements.tileSize()
+ " but got " + width + "x" + height,
fileProbe.path(),
true
));
}
final Map<Integer, Integer> paletteIndexByRgb = new LinkedHashMap<>();
final byte[] paletteIndices = new byte[image.getWidth() * image.getHeight()];
boolean partialAlphaFound = false;
boolean exceededPaletteLimit = false;
int offset = 0;
for (var y = 0; y < image.getHeight(); y++) {
for (var x = 0; x < image.getWidth(); x++) {
final int argb = image.getRGB(x, y);
final int alpha = (argb >>> 24) & 0xFF;
final int rgb = argb & 0x00FFFFFF;
if (alpha == 0 || rgb == COLOR_KEY_RGB) {
paletteIndices[offset++] = 0;
continue;
}
if (alpha < 0xFF) {
partialAlphaFound = true;
}
Integer paletteIndex = paletteIndexByRgb.get(rgb);
if (paletteIndex == null) {
if (paletteIndexByRgb.size() >= MAX_COLORS_PER_PALETTE) {
exceededPaletteLimit = true;
paletteIndices[offset++] = 0;
continue;
}
paletteIndex = paletteIndexByRgb.size() + 1;
paletteIndexByRgb.put(rgb, paletteIndex);
}
paletteIndices[offset++] = paletteIndex.byteValue();
}
}
if (partialAlphaFound) {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.WARNING,
PackerDiagnosticCategory.HYGIENE,
"Tile contains partial alpha; flattening to RGB and ignoring alpha channel",
fileProbe.path(),
false
));
}
if (exceededPaletteLimit) {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.ERROR,
PackerDiagnosticCategory.STRUCTURAL,
"Tile image exceeds color limit for " + fileProbe.path().getFileName()
+ ": expected at most " + MAX_COLORS_PER_PALETTE + " colors for indices 1..15",
fileProbe.path(),
true
));
}
if (diagnostics.stream().anyMatch(PackerDiagnostic::blocking)) {
return new PackerProbeResult(fileProbe, Map.of(), diagnostics);
}
final var tile = new PackerTileIndexedV1(image.getWidth(), image.getHeight(), paletteIndices);
final var palette = buildPalette(paletteIndexByRgb);
serializeProbeArtifacts(fileProbe, tile, palette, diagnostics);
return new PackerProbeResult(fileProbe, Map.of("tile", tile, "palette", palette), diagnostics);
}
private void serializeProbeArtifacts(
final PackerFileProbe fileProbe,
final PackerTileIndexedV1 tile,
final PackerPaletteV1 palette,
final List<PackerDiagnostic> diagnostics) {
final Path basePath = baseOutputPath(fileProbe.path());
final Path tileOutputPath = Path.of(basePath + ".tile.json");
final Path paletteOutputPath = Path.of(basePath + ".palette.json");
try {
Files.createDirectories(Objects.requireNonNull(tileOutputPath.getParent()));
mapper.writerWithDefaultPrettyPrinter().writeValue(tileOutputPath.toFile(), toTilePayload(tile));
mapper.writerWithDefaultPrettyPrinter().writeValue(paletteOutputPath.toFile(), toPalettePayload(palette));
} catch (IOException exception) {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.ERROR,
PackerDiagnosticCategory.STRUCTURAL,
"Unable to serialize tile/palette artifacts for " + fileProbe.path().getFileName()
+ ": " + exception.getMessage(),
fileProbe.path(),
true
));
}
}
private Path baseOutputPath(final Path sourcePath) {
final String fileName = sourcePath.getFileName().toString();
final int extensionStart = fileName.lastIndexOf('.');
final String outputBaseName = extensionStart > 0 ? fileName.substring(0, extensionStart) : fileName;
final Path parent = sourcePath.getParent();
if (parent == null) {
return Path.of(outputBaseName);
}
return parent.resolve(outputBaseName);
}
private Map<String, Object> toTilePayload(final PackerTileIndexedV1 tile) {
final List<Integer> indices = new ArrayList<>(tile.paletteIndices().length);
for (final byte value : tile.paletteIndices()) {
indices.add(Byte.toUnsignedInt(value));
}
return Map.of(
"width", tile.width(),
"height", tile.height(),
"paletteIndices", indices
);
}
private Map<String, Object> toPalettePayload(final PackerPaletteV1 palette) {
final List<Integer> convertedRgb565 = new ArrayList<>(palette.convertedRgb565().length);
for (final short value : palette.convertedRgb565()) {
convertedRgb565.add(Short.toUnsignedInt(value));
}
return Map.of(
"originalArgb8888", palette.originalArgb8888(),
"convertedRgb565", convertedRgb565
);
}
private PackerPaletteV1 buildPalette(final Map<Integer, Integer> paletteIndexByRgb) {
final int[] originalArgb8888 = new int[paletteIndexByRgb.size()];
final short[] convertedRgb565 = new short[paletteIndexByRgb.size()];
for (final var entry : paletteIndexByRgb.entrySet()) {
final int rgb = entry.getKey();
final int index = entry.getValue() - 1;
final int argb = 0xFF000000 | rgb;
originalArgb8888[index] = argb;
convertedRgb565[index] = convertToRgb565(argb);
}
return new PackerPaletteV1(originalArgb8888, convertedRgb565);
}
private short convertToRgb565(final int argb) {
final int red = (argb >> 16) & 0xFF;
final int green = (argb >> 8) & 0xFF;
final int blue = argb & 0xFF;
final int r5 = (red >> 3) & 0x1F;
final int g6 = (green >> 2) & 0x3F;
final int b5 = (blue >> 3) & 0x1F;
return (short) ((r5 << 11) | (g6 << 5) | b5);
}
private java.awt.image.BufferedImage readImage(
final PackerFileProbe fileProbe,
final List<PackerDiagnostic> diagnostics) {
try {
return ImageIO.read(new ByteArrayInputStream(fileProbe.content()));
} catch (Exception exception) {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.ERROR,
PackerDiagnosticCategory.STRUCTURAL,
"Unable to decode tile image content: " + exception.getMessage(),
fileProbe.path(),
true
));
return null;
}
}
}

View File

@ -3,6 +3,7 @@ package p.packer.services;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import p.packer.PackerWorkspacePaths;
import p.packer.PackerWorkspaceService;
import p.packer.events.PackerEventKind;
import p.packer.events.PackerEventSink;
@ -12,6 +13,7 @@ import p.packer.messages.assets.*;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.*;
import p.packer.repositories.PackerRuntimeRegistry;
import java.io.IOException;
import java.nio.file.Files;
@ -57,14 +59,14 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
@Override
public ListAssetsResult listAssets(ListAssetsRequest request) {
final PackerProjectContext project = Objects.requireNonNull(request, "request").project();
final PackerRuntimeSnapshot snapshot = request.deepSync()
final var project = Objects.requireNonNull(request, "request").project();
final var snapshot = request.deepSync()
? runtimeRegistry.refresh(project).snapshot()
: runtimeRegistry.getOrLoad(project).snapshot();
final PackerOperationEventEmitter events = new PackerOperationEventEmitter(project, eventSink);
final PackerRegistryState registry = snapshot.registry();
final var events = new PackerOperationEventEmitter(project, eventSink);
final var registry = snapshot.registry();
final Map<Path, PackerRegistryEntry> registryByRoot = new HashMap<>();
for (PackerRegistryEntry entry : registry.assets()) {
for (final var entry : registry.assets()) {
registryByRoot.put(PackerWorkspacePaths.assetRoot(project, entry.root()), entry);
}
@ -75,15 +77,15 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
final List<PackerRuntimeAsset> runtimeAssets = snapshot.assets();
final int total = runtimeAssets.size();
for (int index = 0; index < runtimeAssets.size(); index += 1) {
final PackerRuntimeAsset runtimeAsset = runtimeAssets.get(index);
final Path assetRoot = runtimeAsset.assetRoot();
final Path assetManifestPath = runtimeAsset.manifestPath();
final var runtimeAsset = runtimeAssets.get(index);
final var assetRoot = runtimeAsset.assetRoot();
final var assetManifestPath = runtimeAsset.manifestPath();
discoveredRoots.add(assetRoot);
final PackerRegistryEntry registryEntry = registryByRoot.get(assetRoot);
final PackerAssetDeclarationParseResult parsed = runtimeAsset.parsedDeclaration();
final var registryEntry = registryByRoot.get(assetRoot);
final var parsed = runtimeAsset.parsedDeclaration();
diagnostics.addAll(parsed.diagnostics());
diagnostics.addAll(identityMismatchDiagnostics(registryEntry, parsed, assetManifestPath));
final PackerAssetSummary summary = buildSummary(project, assetRoot, registryEntry, parsed);
final var summary = buildSummary(project, assetRoot, registryEntry, parsed);
assets.add(summary);
events.emit(
PackerEventKind.ASSET_DISCOVERED,
@ -92,8 +94,8 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
List.of(summary.identity().assetName()));
}
for (PackerRegistryEntry entry : registry.assets()) {
final Path registeredRoot = PackerWorkspacePaths.assetRoot(project, entry.root());
for (final var entry : registry.assets()) {
final var registeredRoot = PackerWorkspacePaths.assetRoot(project, entry.root());
if (!discoveredRoots.contains(registeredRoot)) {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.ERROR,
@ -107,7 +109,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
assets.sort(Comparator
.comparing((PackerAssetSummary asset) -> asset.identity().assetRoot().toString(), String.CASE_INSENSITIVE_ORDER)
.thenComparing(summary -> summary.identity().assetName(), String.CASE_INSENSITIVE_ORDER));
final PackerOperationStatus status = diagnostics.stream().anyMatch(PackerDiagnostic::blocking)
final var status = diagnostics.stream().anyMatch(PackerDiagnostic::blocking)
? PackerOperationStatus.PARTIAL
: PackerOperationStatus.SUCCESS;
if (!diagnostics.isEmpty()) {

View File

@ -1,10 +1,12 @@
package p.packer.services;
import p.packer.PackerWorkspacePaths;
import p.packer.messages.*;
import p.packer.messages.assets.AssetAction;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.*;
import p.packer.repositories.PackerRuntimeRegistry;
import java.nio.file.Path;
import java.util.ArrayList;

View File

@ -16,7 +16,7 @@ import java.nio.file.Path;
import java.util.*;
public final class PackerAssetDeclarationParser {
private static final int SUPPORTED_SCHEMA_VERSION = 1;
private static final Set<Integer> SUPPORTED_SCHEMA_VERSIONS = Set.of(1);
private final ObjectMapper mapper;
@ -40,17 +40,17 @@ public final class PackerAssetDeclarationParser {
return new PackerAssetDeclarationParseResult(null, diagnostics);
}
final Integer schemaVersion = requiredInt(root, "schema_version", diagnostics, manifestPath);
final String assetUuid = requiredText(root, "asset_uuid", diagnostics, manifestPath);
final String name = requiredText(root, "name", diagnostics, manifestPath);
final AssetFamilyCatalog assetFamily = requiredAssetFamily(root, diagnostics, manifestPath);
final Map<String, List<String>> inputsByRole = requiredInputs(root.path("inputs"), diagnostics, manifestPath);
final OutputFormatCatalog outputFormat = requiredOutputFormat(root.path("output"), diagnostics, manifestPath);
final OutputCodecCatalog outputCodec = requiredOutputCodec(root.path("output"), diagnostics, manifestPath);
final Map<String, String> outputMetadata = optionalOutputMetadata(root.path("output"), diagnostics, manifestPath);
final Boolean preloadEnabled = requiredBoolean(root.path("preload"), "enabled", diagnostics, manifestPath);
final var schemaVersion = requiredInt(root, "schema_version", diagnostics, manifestPath);
final var assetUuid = requiredText(root, "asset_uuid", diagnostics, manifestPath);
final var name = requiredText(root, "name", diagnostics, manifestPath);
final var assetFamily = requiredAssetFamily(root, diagnostics, manifestPath);
final var inputsByRole = requiredInputs(root.path("inputs"), diagnostics, manifestPath);
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 preloadEnabled = requiredBoolean(root.path("preload"), "enabled", diagnostics, manifestPath);
if (schemaVersion != null && schemaVersion != SUPPORTED_SCHEMA_VERSION) {
if (schemaVersion != null && !SUPPORTED_SCHEMA_VERSIONS.contains(schemaVersion)) {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.ERROR,
PackerDiagnosticCategory.VERSIONING,
@ -77,7 +77,11 @@ public final class PackerAssetDeclarationParser {
diagnostics);
}
private Integer requiredInt(JsonNode node, String fieldName, List<PackerDiagnostic> diagnostics, Path manifestPath) {
private Integer requiredInt(
final JsonNode node,
final String fieldName,
final List<PackerDiagnostic> diagnostics,
final Path manifestPath) {
final JsonNode field = node.path(fieldName);
if (!field.isInt()) {
diagnostics.add(missingOrInvalid(fieldName, "integer", manifestPath));
@ -86,7 +90,11 @@ public final class PackerAssetDeclarationParser {
return field.intValue();
}
private String requiredText(JsonNode node, String fieldName, List<PackerDiagnostic> diagnostics, Path manifestPath) {
private String requiredText(
final JsonNode node,
final String fieldName,
final List<PackerDiagnostic> diagnostics,
final Path manifestPath) {
final JsonNode field = node.path(fieldName);
if (!field.isTextual() || field.asText().isBlank()) {
diagnostics.add(missingOrInvalid(fieldName, "non-blank string", manifestPath));
@ -95,7 +103,10 @@ public final class PackerAssetDeclarationParser {
return field.asText().trim();
}
private AssetFamilyCatalog requiredAssetFamily(JsonNode node, List<PackerDiagnostic> diagnostics, Path manifestPath) {
private AssetFamilyCatalog requiredAssetFamily(
final JsonNode node,
final List<PackerDiagnostic> diagnostics,
final Path manifestPath) {
final String manifestType = requiredText(node, "type", diagnostics, manifestPath);
if (manifestType == null) {
return null;
@ -113,7 +124,11 @@ public final class PackerAssetDeclarationParser {
return assetFamily;
}
private Boolean requiredBoolean(JsonNode node, String fieldName, List<PackerDiagnostic> diagnostics, Path manifestPath) {
private Boolean requiredBoolean(
final JsonNode node,
final String fieldName,
final List<PackerDiagnostic> diagnostics,
final Path manifestPath) {
final JsonNode field = node.path(fieldName);
if (!field.isBoolean()) {
diagnostics.add(missingOrInvalid(fieldName, "boolean", manifestPath));
@ -122,7 +137,10 @@ public final class PackerAssetDeclarationParser {
return field.booleanValue();
}
private OutputFormatCatalog requiredOutputFormat(JsonNode node, List<PackerDiagnostic> diagnostics, Path manifestPath) {
private OutputFormatCatalog requiredOutputFormat(
final JsonNode node,
final List<PackerDiagnostic> diagnostics,
final Path manifestPath) {
final String fmtValue = requiredText(node, "format", diagnostics, manifestPath);
if (fmtValue == null) {
return null;
@ -140,7 +158,10 @@ public final class PackerAssetDeclarationParser {
return outputFormat;
}
private OutputCodecCatalog requiredOutputCodec(JsonNode node, List<PackerDiagnostic> diagnostics, Path manifestPath) {
private OutputCodecCatalog requiredOutputCodec(
final JsonNode node,
final List<PackerDiagnostic> diagnostics,
final Path manifestPath) {
final String codecValue = requiredText(node, "codec", diagnostics, manifestPath);
if (codecValue == null) {
return null;
@ -159,10 +180,10 @@ public final class PackerAssetDeclarationParser {
}
private Map<String, String> optionalOutputMetadata(
JsonNode outputNode,
List<PackerDiagnostic> diagnostics,
Path manifestPath) {
final JsonNode metadataNode = outputNode.path("metadata");
final JsonNode node,
final List<PackerDiagnostic> diagnostics,
final Path manifestPath) {
final JsonNode metadataNode = node.path("metadata");
if (metadataNode.isMissingNode() || metadataNode.isNull()) {
return Map.of();
}
@ -205,14 +226,17 @@ public final class PackerAssetDeclarationParser {
return Map.copyOf(metadata);
}
private Map<String, List<String>> requiredInputs(JsonNode inputsNode, List<PackerDiagnostic> diagnostics, Path manifestPath) {
if (!inputsNode.isObject()) {
private Map<String, List<String>> requiredInputs(
final JsonNode node,
final List<PackerDiagnostic> diagnostics,
final Path manifestPath) {
if (!node.isObject()) {
diagnostics.add(missingOrInvalid("inputs", "object of input roles", manifestPath));
return Map.of();
}
final Map<String, List<String>> result = new LinkedHashMap<>();
inputsNode.fields().forEachRemaining(entry -> {
node.fields().forEachRemaining(entry -> {
if (!entry.getValue().isArray()) {
diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.ERROR,
@ -250,12 +274,15 @@ public final class PackerAssetDeclarationParser {
return Map.copyOf(result);
}
private boolean isTrustedRelativePath(String value) {
private boolean isTrustedRelativePath(final String value) {
final Path path = Path.of(value).normalize();
return !path.isAbsolute() && !path.startsWith("..");
}
private PackerDiagnostic missingOrInvalid(String fieldName, String expected, Path manifestPath) {
private PackerDiagnostic missingOrInvalid(
final String fieldName,
final String expected,
final Path manifestPath) {
return new PackerDiagnostic(
PackerDiagnosticSeverity.ERROR,
PackerDiagnosticCategory.STRUCTURAL,

View File

@ -1,10 +1,12 @@
package p.packer.services;
import p.packer.PackerWorkspacePaths;
import p.packer.messages.*;
import p.packer.messages.assets.*;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.*;
import p.packer.repositories.PackerRuntimeRegistry;
import java.nio.file.Path;
import java.util.*;

View File

@ -1,5 +1,6 @@
package p.packer.services;
import p.packer.PackerWorkspacePaths;
import p.packer.messages.AssetReference;
import p.packer.messages.PackerProjectContext;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;

View File

@ -1,5 +1,6 @@
package p.packer.services;
import p.packer.PackerWorkspacePaths;
import p.packer.messages.PackerProjectContext;
import p.packer.models.PackerRegistryEntry;
import p.packer.models.PackerRegistryState;

View File

@ -58,13 +58,13 @@ final class PackerOutputContractCatalog {
"tile_size",
"TileSize",
PackerCodecConfigurationFieldType.ENUM,
"16",
"16x16",
true,
List.of("8", "16", "32")));
List.of("8x8", "16x16", "32x32")));
case SOUND_V1 -> List.of(
new PackerCodecConfigurationField(
"sample_rate",
"Frame Rate",
"Sample Rate",
PackerCodecConfigurationFieldType.ENUM,
"44100",
true,

View File

@ -1,5 +1,6 @@
package p.packer.services;
import p.packer.PackerWorkspacePaths;
import p.packer.messages.PackerProjectContext;
import p.packer.models.PackerRegistryEntry;
import p.packer.models.PackerRegistryState;

View File

@ -1,11 +1,14 @@
package p.packer.services;
import com.fasterxml.jackson.databind.ObjectMapper;
import p.packer.PackerWorkspacePaths;
import p.packer.messages.InitWorkspaceRequest;
import p.packer.messages.InitWorkspaceResult;
import p.packer.messages.PackerOperationStatus;
import p.packer.messages.PackerProjectContext;
import p.packer.models.PackerRegistryEntry;
import p.packer.models.PackerRegistryState;
import p.packer.repositories.FileSystemPackerRegistryRepository;
import java.nio.file.Files;
import java.nio.file.Path;
@ -17,8 +20,8 @@ public final class PackerWorkspaceFoundation {
private final PackerIdentityAllocator identityAllocator;
private final PackerRegistryLookup registryLookup;
public PackerWorkspaceFoundation() {
this(new FileSystemPackerRegistryRepository(), new PackerIdentityAllocator(), new PackerRegistryLookup());
public PackerWorkspaceFoundation(final ObjectMapper mapper) {
this(new FileSystemPackerRegistryRepository(mapper), new PackerIdentityAllocator(), new PackerRegistryLookup());
}
public PackerWorkspaceFoundation(

View File

@ -7,6 +7,10 @@ import p.packer.events.PackerEvent;
import p.packer.events.PackerEventKind;
import p.packer.messages.*;
import p.packer.messages.assets.*;
import p.packer.repositories.PackerAssetWalker;
import p.packer.repositories.PackerRuntimeLoader;
import p.packer.repositories.PackerRuntimeRegistry;
import p.packer.repositories.PackerRuntimeSnapshotLoader;
import p.packer.testing.PackerFixtureLocator;
import java.nio.file.Files;
@ -193,7 +197,7 @@ final class FileSystemPackerWorkspaceServiceTest {
"NONE:packMode", "tight",
"NONE:palette", "mono"),
Map.of(
"tile_size", "16",
"tile_size", "16x16",
"channels", "1")));
assertTrue(result.success());
@ -205,7 +209,7 @@ final class FileSystemPackerWorkspaceServiceTest {
assertEquals("NONE", manifest.path("output").path("codec").asText());
assertEquals("tight", manifest.path("output").path("codec_configuration").path("packMode").asText());
assertEquals("mono", manifest.path("output").path("codec_configuration").path("palette").asText());
assertEquals("16", manifest.path("output").path("metadata").path("tile_size").asText());
assertEquals("16x16", manifest.path("output").path("metadata").path("tile_size").asText());
assertEquals("1", manifest.path("output").path("metadata").path("channels").asText());
}
@ -672,9 +676,10 @@ final class FileSystemPackerWorkspaceServiceTest {
private FileSystemPackerWorkspaceService service(
p.packer.events.PackerEventSink eventSink,
PackerRuntimeSnapshotLoader loader) {
final var foundation = new p.packer.services.PackerWorkspaceFoundation();
final var parser = new p.packer.services.PackerAssetDeclarationParser(new ObjectMapper());
final var runtimeRegistry = new p.packer.services.PackerRuntimeRegistry(loader);
final var mapper = new ObjectMapper();
final var foundation = new p.packer.services.PackerWorkspaceFoundation(mapper);
final var parser = new p.packer.services.PackerAssetDeclarationParser(mapper);
final var runtimeRegistry = new PackerRuntimeRegistry(loader);
final var resolver = new p.packer.services.PackerAssetReferenceResolver(foundation.lookup());
final var detailsService = new p.packer.services.PackerAssetDetailsService(runtimeRegistry, resolver);
final var actionReadService = new p.packer.services.PackerAssetActionReadService(runtimeRegistry, resolver, foundation.lookup());
@ -692,9 +697,11 @@ final class FileSystemPackerWorkspaceServiceTest {
}
private CountingLoader countingLoader() {
final var foundation = new p.packer.services.PackerWorkspaceFoundation();
final var parser = new p.packer.services.PackerAssetDeclarationParser(new ObjectMapper());
return new CountingLoader(new p.packer.services.PackerRuntimeLoader(foundation, parser));
final var mapper = new ObjectMapper();
final var foundation = new p.packer.services.PackerWorkspaceFoundation(mapper);
final var parser = new p.packer.services.PackerAssetDeclarationParser(mapper);
final var assetWalker = new PackerAssetWalker(mapper);
return new CountingLoader(new PackerRuntimeLoader(foundation, parser, assetWalker));
}
private static final class CountingLoader implements PackerRuntimeSnapshotLoader {

View File

@ -11,6 +11,9 @@ import p.packer.messages.PackerProjectContext;
import p.packer.messages.assets.OutputCodecCatalog;
import p.packer.messages.assets.PackerAssetState;
import p.packer.messages.assets.PackerBuildParticipation;
import p.packer.repositories.PackerAssetWalker;
import p.packer.repositories.PackerRuntimeLoader;
import p.packer.repositories.PackerRuntimeRegistry;
import p.packer.testing.PackerFixtureLocator;
import java.nio.file.Files;
@ -68,7 +71,7 @@ final class PackerAssetDetailsServiceTest {
final var manifest = mapper.readTree(manifestPath.toFile());
final ObjectNode output = (ObjectNode) manifest.path("output");
final ObjectNode metadata = output.putObject("metadata");
metadata.put("tile_size", "16");
metadata.put("tile_size", "16x16");
metadata.put("channels", "1");
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
@ -77,7 +80,7 @@ final class PackerAssetDetailsServiceTest {
assertEquals(PackerOperationStatus.SUCCESS, result.status());
assertEquals(
Map.of("tile_size", "16", "channels", "1"),
Map.of("tile_size", "16x16", "channels", "1"),
result.details().metadataFields().stream().collect(java.util.stream.Collectors.toMap(
field -> field.key(),
field -> field.value())));
@ -130,9 +133,11 @@ final class PackerAssetDetailsServiceTest {
}
private PackerAssetDetailsService service() {
final var foundation = new p.packer.services.PackerWorkspaceFoundation();
final var parser = new PackerAssetDeclarationParser(new ObjectMapper());
final var runtimeRegistry = new PackerRuntimeRegistry(new PackerRuntimeLoader(foundation, parser));
final var mapper = new ObjectMapper();
final var foundation = new PackerWorkspaceFoundation(mapper);
final var parser = new PackerAssetDeclarationParser(mapper);
final var assetWalker = new PackerAssetWalker(mapper);
final var runtimeRegistry = new PackerRuntimeRegistry(new PackerRuntimeLoader(foundation, parser, assetWalker));
final var resolver = new PackerAssetReferenceResolver(foundation.lookup());
return new PackerAssetDetailsService(runtimeRegistry, resolver);
}

View File

@ -4,6 +4,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.packer.messages.PackerProjectContext;
import p.packer.repositories.PackerAssetWalker;
import p.packer.repositories.PackerRuntimeLoader;
import p.packer.repositories.PackerRuntimeRegistry;
import p.packer.testing.PackerFixtureLocator;
import java.nio.file.Files;
@ -78,9 +81,11 @@ final class PackerRuntimeRegistryTest {
}
private PackerRuntimeRegistry runtimeRegistry() {
final var foundation = new PackerWorkspaceFoundation();
final var parser = new PackerAssetDeclarationParser(new ObjectMapper());
return new PackerRuntimeRegistry(new PackerRuntimeLoader(foundation, parser));
final var mapper = new ObjectMapper();
final var foundation = new PackerWorkspaceFoundation(mapper);
final var parser = new PackerAssetDeclarationParser(mapper);
final var assetWalker = new PackerAssetWalker(mapper);
return new PackerRuntimeRegistry(new PackerRuntimeLoader(foundation, parser, assetWalker));
}
private PackerProjectContext project(Path root) {

View File

@ -1,11 +1,13 @@
package p.packer.services;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.packer.messages.InitWorkspaceRequest;
import p.packer.messages.PackerProjectContext;
import p.packer.models.PackerRegistryEntry;
import p.packer.models.PackerRegistryState;
import p.packer.repositories.FileSystemPackerRegistryRepository;
import java.nio.file.Files;
import java.nio.file.Path;
@ -18,7 +20,8 @@ final class PackerWorkspaceFoundationTest {
@Test
void initWorkspaceCreatesAssetsControlStructureAndRegistry() throws Exception {
final PackerWorkspaceFoundation foundation = new PackerWorkspaceFoundation();
final var mapper = new ObjectMapper();
final PackerWorkspaceFoundation foundation = new PackerWorkspaceFoundation(mapper);
final PackerProjectContext project = project(tempDir.resolve("main"));
final var result = foundation.initWorkspace(new InitWorkspaceRequest(project));
@ -35,7 +38,8 @@ final class PackerWorkspaceFoundationTest {
@Test
void registryRoundTripPreservesAllocatorAndEntries() throws Exception {
final Path projectRoot = tempDir.resolve("main");
final PackerWorkspaceFoundation foundation = new PackerWorkspaceFoundation();
final var mapper = new ObjectMapper();
final PackerWorkspaceFoundation foundation = new PackerWorkspaceFoundation(mapper);
final PackerProjectContext project = project(projectRoot);
foundation.initWorkspace(new InitWorkspaceRequest(project));
@ -58,7 +62,8 @@ final class PackerWorkspaceFoundationTest {
@Test
void allocatorIsMonotonicAndPersistedAcrossSaveLoad() throws Exception {
final Path projectRoot = tempDir.resolve("main");
final PackerWorkspaceFoundation foundation = new PackerWorkspaceFoundation();
final var mapper = new ObjectMapper();
final PackerWorkspaceFoundation foundation = new PackerWorkspaceFoundation(mapper);
final PackerProjectContext project = project(projectRoot);
foundation.initWorkspace(new InitWorkspaceRequest(project));
Files.createDirectories(projectRoot.resolve("assets/ui/atlas"));
@ -89,7 +94,8 @@ final class PackerWorkspaceFoundationTest {
]
}
""");
final FileSystemPackerRegistryRepository repository = new FileSystemPackerRegistryRepository();
final var mapper = new ObjectMapper();
final FileSystemPackerRegistryRepository repository = new FileSystemPackerRegistryRepository(mapper);
final p.packer.exceptions.PackerRegistryException exception = assertThrows(p.packer.exceptions.PackerRegistryException.class, () -> repository.load(project(projectRoot)));
@ -101,7 +107,8 @@ final class PackerWorkspaceFoundationTest {
final Path projectRoot = tempDir.resolve("main");
Files.createDirectories(projectRoot.resolve("assets/.prometeu"));
Files.writeString(projectRoot.resolve("assets/.prometeu/index.json"), "{ nope ");
final FileSystemPackerRegistryRepository repository = new FileSystemPackerRegistryRepository();
final var mapper = new ObjectMapper();
final FileSystemPackerRegistryRepository repository = new FileSystemPackerRegistryRepository(mapper);
final p.packer.exceptions.PackerRegistryException exception = assertThrows(p.packer.exceptions.PackerRegistryException.class, () -> repository.load(project(projectRoot)));
@ -119,7 +126,8 @@ final class PackerWorkspaceFoundationTest {
"assets": []
}
""");
final FileSystemPackerRegistryRepository repository = new FileSystemPackerRegistryRepository();
final var mapper = new ObjectMapper();
final FileSystemPackerRegistryRepository repository = new FileSystemPackerRegistryRepository(mapper);
final p.packer.exceptions.PackerRegistryException exception = assertThrows(p.packer.exceptions.PackerRegistryException.class, () -> repository.load(project(projectRoot)));
@ -139,7 +147,8 @@ final class PackerWorkspaceFoundationTest {
]
}
""");
final FileSystemPackerRegistryRepository repository = new FileSystemPackerRegistryRepository();
final var mapper = new ObjectMapper();
final FileSystemPackerRegistryRepository repository = new FileSystemPackerRegistryRepository(mapper);
final p.packer.exceptions.PackerRegistryException exception = assertThrows(p.packer.exceptions.PackerRegistryException.class, () -> repository.load(project(projectRoot)));
@ -149,7 +158,8 @@ final class PackerWorkspaceFoundationTest {
@Test
void lookupResolvesByIdUuidAndRootAndFailsOnMissingRoot() throws Exception {
final Path projectRoot = tempDir.resolve("main");
final PackerWorkspaceFoundation foundation = new PackerWorkspaceFoundation();
final var mapper = new ObjectMapper();
final PackerWorkspaceFoundation foundation = new PackerWorkspaceFoundation(mapper);
final PackerProjectContext project = project(projectRoot);
foundation.initWorkspace(new InitWorkspaceRequest(project));
Files.createDirectories(projectRoot.resolve("assets/ui/atlas"));

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@
"codec" : "NONE",
"codec_configuration" : { },
"metadata" : {
"tile_size" : "16"
"tile_size" : "16x16"
}
},
"preload" : {

View File

@ -0,0 +1,4 @@
{
"convertedRgb565" : [ 65414 ],
"originalArgb8888" : [ -265674 ]
}

View File

Before

Width:  |  Height:  |  Size: 137 B

After

Width:  |  Height:  |  Size: 137 B

View File

@ -0,0 +1,5 @@
{
"height" : 16,
"width" : 16,
"paletteIndices" : [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 ]
}

View File

@ -3,7 +3,7 @@
"asset_uuid": "21953cb8-4101-4790-9e5e-d95f5fbc9b5a",
"name": "ui_atlas",
"type": "tile_bank",
"inputs": { "sprites": ["sprites/confirm.png"] },
"inputs": { },
"output": { "format": "TILES/indexed_v1", "codec": "NONE" },
"preload": { "enabled": true }
}

View File

@ -0,0 +1,4 @@
{
"convertedRgb565" : [ 65414 ],
"originalArgb8888" : [ -265674 ]
}

View File

Before

Width:  |  Height:  |  Size: 137 B

After

Width:  |  Height:  |  Size: 137 B

View File

@ -0,0 +1,5 @@
{
"height" : 16,
"width" : 16,
"paletteIndices" : [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 ]
}