implements PR-027 runtime walkresult and cache integration

This commit is contained in:
bQUARKz 2026-03-18 19:56:52 +00:00
parent 1442adc7b3
commit b4733a0e49
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
18 changed files with 512 additions and 52 deletions

View File

@ -53,6 +53,9 @@ public enum OutputFormatCatalog {
return UNKNOWN; return UNKNOWN;
} }
final String normalized = manifestValue.trim().toLowerCase(Locale.ROOT); final String normalized = manifestValue.trim().toLowerCase(Locale.ROOT);
if ("sound/bank_v1".equals(normalized)) {
return SOUND_V1;
}
for (OutputFormatCatalog candidate : values()) { for (OutputFormatCatalog candidate : values()) {
if (candidate == UNKNOWN) { if (candidate == UNKNOWN) {
continue; continue;

View File

@ -2,7 +2,9 @@ package p.packer;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import p.packer.events.PackerEventSink; import p.packer.events.PackerEventSink;
import p.packer.repositories.FileSystemPackerCacheRepository;
import p.packer.repositories.PackerAssetWalker; import p.packer.repositories.PackerAssetWalker;
import p.packer.repositories.PackerRuntimeAssetMaterializer;
import p.packer.repositories.PackerRuntimeLoader; import p.packer.repositories.PackerRuntimeLoader;
import p.packer.repositories.PackerRuntimeRegistry; import p.packer.repositories.PackerRuntimeRegistry;
import p.packer.services.*; import p.packer.services.*;
@ -29,7 +31,13 @@ public final class Packer implements Closeable {
final var workspaceFoundation = new PackerWorkspaceFoundation(mapper); final var workspaceFoundation = new PackerWorkspaceFoundation(mapper);
final var declarationParser = new PackerAssetDeclarationParser(mapper); final var declarationParser = new PackerAssetDeclarationParser(mapper);
final var assetWalker = new PackerAssetWalker(mapper); final var assetWalker = new PackerAssetWalker(mapper);
final var runtimeLoader = new PackerRuntimeLoader(workspaceFoundation, declarationParser, assetWalker); final var cacheRepository = new FileSystemPackerCacheRepository(mapper);
final var assetMaterializer = new PackerRuntimeAssetMaterializer(assetWalker);
final var runtimeLoader = new PackerRuntimeLoader(
workspaceFoundation,
declarationParser,
cacheRepository,
assetMaterializer);
final var runtimeRegistry = new PackerRuntimeRegistry(runtimeLoader); final var runtimeRegistry = new PackerRuntimeRegistry(runtimeLoader);
final var assetReferenceResolver = new PackerAssetReferenceResolver(workspaceFoundation.lookup()); final var assetReferenceResolver = new PackerAssetReferenceResolver(workspaceFoundation.lookup());
final var assetDetailsService = new PackerAssetDetailsService(runtimeRegistry, assetReferenceResolver); final var assetDetailsService = new PackerAssetDetailsService(runtimeRegistry, assetReferenceResolver);
@ -37,7 +45,7 @@ public final class Packer implements Closeable {
runtimeRegistry, runtimeRegistry,
assetReferenceResolver, assetReferenceResolver,
workspaceFoundation.lookup()); workspaceFoundation.lookup());
final var runtimePatchService = new PackerRuntimePatchService(declarationParser); final var runtimePatchService = new PackerRuntimePatchService(declarationParser, assetMaterializer);
final var writeCoordinator = new PackerProjectWriteCoordinator(); final var writeCoordinator = new PackerProjectWriteCoordinator();
return new Packer(new FileSystemPackerWorkspaceService( return new Packer(new FileSystemPackerWorkspaceService(
mapper, mapper,
@ -46,6 +54,7 @@ public final class Packer implements Closeable {
assetActionReadService, assetActionReadService,
runtimePatchService, runtimePatchService,
runtimeRegistry, runtimeRegistry,
cacheRepository,
writeCoordinator, writeCoordinator,
resolvedEventSink), runtimeRegistry, writeCoordinator); resolvedEventSink), runtimeRegistry, writeCoordinator);
} }

View File

@ -1,6 +1,7 @@
package p.packer.models; package p.packer.models;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -8,12 +9,24 @@ public record PackerRuntimeAsset(
Path assetRoot, Path assetRoot,
Path manifestPath, Path manifestPath,
Optional<PackerRegistryEntry> registryEntry, Optional<PackerRegistryEntry> registryEntry,
PackerAssetDeclarationParseResult parsedDeclaration) { PackerAssetDeclarationParseResult parsedDeclaration,
PackerRuntimeWalkProjection walkProjection,
List<PackerDiagnostic> walkDiagnostics) {
public PackerRuntimeAsset(
Path assetRoot,
Path manifestPath,
Optional<PackerRegistryEntry> registryEntry,
PackerAssetDeclarationParseResult parsedDeclaration) {
this(assetRoot, manifestPath, registryEntry, parsedDeclaration, PackerRuntimeWalkProjection.EMPTY, List.of());
}
public PackerRuntimeAsset { public PackerRuntimeAsset {
assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize(); assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
manifestPath = Objects.requireNonNull(manifestPath, "manifestPath").toAbsolutePath().normalize(); manifestPath = Objects.requireNonNull(manifestPath, "manifestPath").toAbsolutePath().normalize();
registryEntry = Objects.requireNonNull(registryEntry, "registryEntry"); registryEntry = Objects.requireNonNull(registryEntry, "registryEntry");
parsedDeclaration = Objects.requireNonNull(parsedDeclaration, "parsedDeclaration"); parsedDeclaration = Objects.requireNonNull(parsedDeclaration, "parsedDeclaration");
walkProjection = Objects.requireNonNull(walkProjection, "walkProjection");
walkDiagnostics = List.copyOf(Objects.requireNonNull(walkDiagnostics, "walkDiagnostics"));
} }
} }

View File

@ -6,7 +6,15 @@ import java.util.Objects;
public record PackerRuntimeSnapshot( public record PackerRuntimeSnapshot(
long generation, long generation,
PackerRegistryState registry, PackerRegistryState registry,
List<PackerRuntimeAsset> assets) { List<PackerRuntimeAsset> assets,
PackerWorkspaceCacheState cacheState) {
public PackerRuntimeSnapshot(
long generation,
PackerRegistryState registry,
List<PackerRuntimeAsset> assets) {
this(generation, registry, assets, PackerWorkspaceCacheState.EMPTY);
}
public PackerRuntimeSnapshot { public PackerRuntimeSnapshot {
if (generation <= 0L) { if (generation <= 0L) {
@ -14,5 +22,6 @@ public record PackerRuntimeSnapshot(
} }
registry = Objects.requireNonNull(registry, "registry"); registry = Objects.requireNonNull(registry, "registry");
assets = List.copyOf(Objects.requireNonNull(assets, "assets")); assets = List.copyOf(Objects.requireNonNull(assets, "assets"));
cacheState = Objects.requireNonNull(cacheState, "cacheState");
} }
} }

View File

@ -0,0 +1,30 @@
package p.packer.models;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public record PackerRuntimeWalkFile(
String relativePath,
String mimeType,
long size,
long lastModified,
String fingerprint,
Map<String, Object> metadata,
List<PackerDiagnostic> diagnostics) {
public PackerRuntimeWalkFile {
relativePath = Objects.requireNonNull(relativePath, "relativePath");
mimeType = mimeType == null || mimeType.isBlank() ? null : mimeType;
if (size < 0L) {
throw new IllegalArgumentException("size must be non-negative");
}
if (lastModified < 0L) {
throw new IllegalArgumentException("lastModified must be non-negative");
}
fingerprint = fingerprint == null || fingerprint.isBlank() ? null : fingerprint;
metadata = Map.copyOf(new LinkedHashMap<>(Objects.requireNonNull(metadata, "metadata")));
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
}
}

View File

@ -0,0 +1,19 @@
package p.packer.models;
import java.util.List;
import java.util.Objects;
public record PackerRuntimeWalkProjection(
List<String> availableFiles,
List<PackerRuntimeWalkFile> buildCandidateFiles,
long measuredBankSizeBytes) {
public static final PackerRuntimeWalkProjection EMPTY = new PackerRuntimeWalkProjection(List.of(), List.of(), 0L);
public PackerRuntimeWalkProjection {
availableFiles = List.copyOf(Objects.requireNonNull(availableFiles, "availableFiles"));
buildCandidateFiles = List.copyOf(Objects.requireNonNull(buildCandidateFiles, "buildCandidateFiles"));
if (measuredBankSizeBytes < 0L) {
throw new IllegalArgumentException("measuredBankSizeBytes must be non-negative");
}
}
}

View File

@ -14,9 +14,6 @@ import p.packer.models.PackerWalkResult;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -132,12 +129,7 @@ public abstract class PackerAbstractBankWalker<R> {
} }
protected String computeFingerprint(final PackerFileProbe fileProbe) { protected String computeFingerprint(final PackerFileProbe fileProbe) {
try { return PackerFileFingerprint.sha256(fileProbe);
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
return HexFormat.of().formatHex(digest.digest(fileProbe.content()));
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 fingerprint is unavailable", exception);
}
} }
private Optional<PackerFileCacheEntry> resolvePriorFileCache( private Optional<PackerFileCacheEntry> resolvePriorFileCache(

View File

@ -146,7 +146,7 @@ public class PackerAssetWalker {
return RequirementBuildResult.fail("Missing sample rate for sound bank"); return RequirementBuildResult.fail("Missing sample rate for sound bank");
} }
final var sampleRate = Integer.parseInt(sampleRateStr); final var sampleRate = Integer.parseInt(sampleRateStr);
final var channelsStr = metadata.get(""); final var channelsStr = metadata.get("channels");
if (StringUtils.isBlank(channelsStr)) { if (StringUtils.isBlank(channelsStr)) {
return RequirementBuildResult.fail("Missing channels for sound bank"); return RequirementBuildResult.fail("Missing channels for sound bank");
} }

View File

@ -0,0 +1,21 @@
package p.packer.repositories;
import p.packer.models.PackerFileProbe;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
final class PackerFileFingerprint {
private PackerFileFingerprint() {
}
static String sha256(final PackerFileProbe fileProbe) {
try {
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
return HexFormat.of().formatHex(digest.digest(fileProbe.content()));
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 fingerprint is unavailable", exception);
}
}
}

View File

@ -0,0 +1,110 @@
package p.packer.repositories;
import p.packer.models.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
public final class PackerRuntimeAssetMaterializer {
private final PackerAssetWalker assetWalker;
public PackerRuntimeAssetMaterializer(PackerAssetWalker assetWalker) {
this.assetWalker = Objects.requireNonNull(assetWalker, "assetWalker");
}
public PackerRuntimeAssetMaterialization materialize(
Path assetRoot,
Path manifestPath,
Optional<PackerRegistryEntry> registryEntry,
PackerAssetDeclarationParseResult parseResult,
Optional<PackerAssetCacheEntry> priorAssetCache) {
final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
final Path normalizedManifestPath = Objects.requireNonNull(manifestPath, "manifestPath").toAbsolutePath().normalize();
final Optional<PackerRegistryEntry> safeRegistryEntry = Objects.requireNonNull(registryEntry, "registryEntry");
final PackerAssetDeclarationParseResult safeParseResult = Objects.requireNonNull(parseResult, "parseResult");
final Optional<PackerAssetCacheEntry> safePriorAssetCache = Objects.requireNonNull(priorAssetCache, "priorAssetCache");
if (safeRegistryEntry.isEmpty() || !safeParseResult.valid()) {
return new PackerRuntimeAssetMaterialization(
new PackerRuntimeAsset(
normalizedAssetRoot,
normalizedManifestPath,
safeRegistryEntry,
safeParseResult,
PackerRuntimeWalkProjection.EMPTY,
List.of()),
Optional.empty());
}
final List<String> availableFiles = listAvailableFiles(normalizedAssetRoot);
final PackerWalkResult walkResult = assetWalker.walk(
normalizedAssetRoot,
safeParseResult.declaration(),
safePriorAssetCache);
final List<PackerRuntimeWalkFile> buildCandidateFiles = walkResult.probeResults().stream()
.map(probeResult -> toRuntimeWalkFile(normalizedAssetRoot, probeResult))
.sorted(Comparator.comparing(PackerRuntimeWalkFile::relativePath, String.CASE_INSENSITIVE_ORDER))
.toList();
final long measuredBankSizeBytes = buildCandidateFiles.stream()
.mapToLong(PackerRuntimeWalkFile::size)
.sum();
final PackerRuntimeWalkProjection walkProjection = new PackerRuntimeWalkProjection(
availableFiles,
buildCandidateFiles,
measuredBankSizeBytes);
final PackerRuntimeAsset runtimeAsset = new PackerRuntimeAsset(
normalizedAssetRoot,
normalizedManifestPath,
safeRegistryEntry,
safeParseResult,
walkProjection,
walkResult.diagnostics());
final PackerAssetCacheEntry assetCacheEntry = new PackerAssetCacheEntry(
safeRegistryEntry.get().assetId(),
buildCandidateFiles.stream()
.map(file -> new PackerFileCacheEntry(
file.relativePath(),
file.mimeType(),
file.size(),
file.lastModified(),
file.fingerprint(),
file.metadata()))
.toList());
return new PackerRuntimeAssetMaterialization(runtimeAsset, Optional.of(assetCacheEntry));
}
private List<String> listAvailableFiles(Path assetRoot) {
try (var paths = Files.list(assetRoot)
.filter(Files::isRegularFile)
.filter(path -> !path.getFileName().toString().equalsIgnoreCase("asset.json"))
.map(path -> assetRoot.relativize(path.toAbsolutePath().normalize()).toString().replace('\\', '/'))
.sorted(String.CASE_INSENSITIVE_ORDER)) {
return paths.toList();
} catch (IOException exception) {
return List.of();
}
}
private PackerRuntimeWalkFile toRuntimeWalkFile(Path assetRoot, PackerProbeResult probeResult) {
final PackerFileProbe fileProbe = probeResult.fileProbe();
return new PackerRuntimeWalkFile(
assetRoot.relativize(fileProbe.path().toAbsolutePath().normalize()).toString().replace('\\', '/'),
fileProbe.mimeType(),
fileProbe.size(),
fileProbe.lastModified(),
PackerFileFingerprint.sha256(fileProbe),
probeResult.metadata(),
probeResult.diagnostics());
}
public record PackerRuntimeAssetMaterialization(
PackerRuntimeAsset runtimeAsset,
Optional<PackerAssetCacheEntry> assetCacheEntry) {
public PackerRuntimeAssetMaterialization {
runtimeAsset = Objects.requireNonNull(runtimeAsset, "runtimeAsset");
assetCacheEntry = Objects.requireNonNull(assetCacheEntry, "assetCacheEntry");
}
}
}

View File

@ -2,8 +2,10 @@ package p.packer.repositories;
import p.packer.messages.InitWorkspaceRequest; import p.packer.messages.InitWorkspaceRequest;
import p.packer.messages.PackerProjectContext; import p.packer.messages.PackerProjectContext;
import p.packer.models.PackerAssetCacheEntry;
import p.packer.models.PackerRuntimeAsset; import p.packer.models.PackerRuntimeAsset;
import p.packer.models.PackerRuntimeSnapshot; import p.packer.models.PackerRuntimeSnapshot;
import p.packer.models.PackerWorkspaceCacheState;
import p.packer.services.PackerAssetDeclarationParser; import p.packer.services.PackerAssetDeclarationParser;
import p.packer.services.PackerWorkspaceFoundation; import p.packer.services.PackerWorkspaceFoundation;
import p.packer.PackerWorkspacePaths; import p.packer.PackerWorkspacePaths;
@ -18,15 +20,18 @@ import java.util.stream.Collectors;
public final class PackerRuntimeLoader implements PackerRuntimeSnapshotLoader { public final class PackerRuntimeLoader implements PackerRuntimeSnapshotLoader {
private final PackerWorkspaceFoundation workspaceFoundation; private final PackerWorkspaceFoundation workspaceFoundation;
private final PackerAssetDeclarationParser parser; private final PackerAssetDeclarationParser parser;
private final PackerAssetWalker assetWalker; private final FileSystemPackerCacheRepository cacheRepository;
private final PackerRuntimeAssetMaterializer assetMaterializer;
public PackerRuntimeLoader( public PackerRuntimeLoader(
final PackerWorkspaceFoundation workspaceFoundation, final PackerWorkspaceFoundation workspaceFoundation,
final PackerAssetDeclarationParser parser, final PackerAssetDeclarationParser parser,
final PackerAssetWalker assetWalker) { final FileSystemPackerCacheRepository cacheRepository,
final PackerRuntimeAssetMaterializer assetMaterializer) {
this.workspaceFoundation = Objects.requireNonNull(workspaceFoundation, "workspaceFoundation"); this.workspaceFoundation = Objects.requireNonNull(workspaceFoundation, "workspaceFoundation");
this.parser = Objects.requireNonNull(parser, "parser"); this.parser = Objects.requireNonNull(parser, "parser");
this.assetWalker = Objects.requireNonNull(assetWalker, "assetWalker"); this.cacheRepository = Objects.requireNonNull(cacheRepository, "cacheRepository");
this.assetMaterializer = Objects.requireNonNull(assetMaterializer, "assetMaterializer");
} }
private boolean isAssetJson(Path path, BasicFileAttributes attrs) { private boolean isAssetJson(Path path, BasicFileAttributes attrs) {
@ -39,6 +44,7 @@ public final class PackerRuntimeLoader implements PackerRuntimeSnapshotLoader {
workspaceFoundation.initWorkspace(new InitWorkspaceRequest(safeProject)); workspaceFoundation.initWorkspace(new InitWorkspaceRequest(safeProject));
final var registry = workspaceFoundation.loadRegistry(safeProject); final var registry = workspaceFoundation.loadRegistry(safeProject);
final PackerWorkspaceCacheState priorCacheState = loadPriorCacheState(safeProject);
final var registryByRoot = registry final var registryByRoot = registry
.assets() .assets()
.stream() .stream()
@ -47,6 +53,7 @@ public final class PackerRuntimeLoader implements PackerRuntimeSnapshotLoader {
entry -> entry)); entry -> entry));
final List<PackerRuntimeAsset> assets = new ArrayList<>(); final List<PackerRuntimeAsset> assets = new ArrayList<>();
final List<PackerAssetCacheEntry> refreshedCacheEntries = new ArrayList<>();
final var assetsRoot = PackerWorkspacePaths.assetsRoot(safeProject); final var assetsRoot = PackerWorkspacePaths.assetsRoot(safeProject);
if (Files.isDirectory(assetsRoot)) { if (Files.isDirectory(assetsRoot)) {
try (final var paths = Files.find(assetsRoot, Integer.MAX_VALUE, this::isAssetJson)) { try (final var paths = Files.find(assetsRoot, Integer.MAX_VALUE, this::isAssetJson)) {
@ -58,12 +65,16 @@ public final class PackerRuntimeLoader implements PackerRuntimeSnapshotLoader {
final var assetRoot = manifestPath.getParent(); final var assetRoot = manifestPath.getParent();
final var registryEntry = Optional.ofNullable(registryByRoot.get(assetRoot)); final var registryEntry = Optional.ofNullable(registryByRoot.get(assetRoot));
final var parseResult = parser.parse(manifestPath); final var parseResult = parser.parse(manifestPath);
if (parseResult.valid()) { final Optional<PackerAssetCacheEntry> priorAssetCache = registryEntry
final var walkResult = assetWalker.walk(assetRoot, parseResult.declaration()); .flatMap(entry -> priorCacheState.findAsset(entry.assetId()));
final var materialized = assetMaterializer.materialize(
} assetRoot,
final var runtimeAsset = new PackerRuntimeAsset(assetRoot, manifestPath, registryEntry, parseResult); manifestPath,
assets.add(runtimeAsset); registryEntry,
parseResult,
priorAssetCache);
assets.add(materialized.runtimeAsset());
materialized.assetCacheEntry().ifPresent(refreshedCacheEntries::add);
} }
} catch (IOException exception) { } catch (IOException exception) {
throw new p.packer.exceptions.PackerRegistryException( throw new p.packer.exceptions.PackerRegistryException(
@ -72,6 +83,20 @@ public final class PackerRuntimeLoader implements PackerRuntimeSnapshotLoader {
} }
} }
return new PackerRuntimeSnapshot(generation, registry, assets); final PackerWorkspaceCacheState refreshedCacheState = new PackerWorkspaceCacheState(
PackerWorkspaceCacheState.CURRENT_SCHEMA_VERSION,
refreshedCacheEntries.stream()
.sorted(Comparator.comparingInt(PackerAssetCacheEntry::assetId))
.toList());
cacheRepository.save(safeProject, refreshedCacheState);
return new PackerRuntimeSnapshot(generation, registry, assets, refreshedCacheState);
}
private PackerWorkspaceCacheState loadPriorCacheState(PackerProjectContext project) {
try {
return cacheRepository.load(project);
} catch (p.packer.exceptions.PackerRegistryException exception) {
return PackerWorkspaceCacheState.EMPTY;
}
} }
} }

View File

@ -13,6 +13,7 @@ import p.packer.messages.assets.*;
import p.packer.messages.diagnostics.PackerDiagnosticCategory; import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity; import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.*; import p.packer.models.*;
import p.packer.repositories.FileSystemPackerCacheRepository;
import p.packer.repositories.PackerRuntimeRegistry; import p.packer.repositories.PackerRuntimeRegistry;
import java.io.IOException; import java.io.IOException;
@ -28,6 +29,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
private final PackerAssetActionReadService actionReadService; private final PackerAssetActionReadService actionReadService;
private final PackerRuntimePatchService runtimePatchService; private final PackerRuntimePatchService runtimePatchService;
private final PackerRuntimeRegistry runtimeRegistry; private final PackerRuntimeRegistry runtimeRegistry;
private final FileSystemPackerCacheRepository cacheRepository;
private final PackerProjectWriteCoordinator writeCoordinator; private final PackerProjectWriteCoordinator writeCoordinator;
private final PackerEventSink eventSink; private final PackerEventSink eventSink;
@ -38,6 +40,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
PackerAssetActionReadService actionReadService, PackerAssetActionReadService actionReadService,
PackerRuntimePatchService runtimePatchService, PackerRuntimePatchService runtimePatchService,
PackerRuntimeRegistry runtimeRegistry, PackerRuntimeRegistry runtimeRegistry,
FileSystemPackerCacheRepository cacheRepository,
PackerProjectWriteCoordinator writeCoordinator, PackerProjectWriteCoordinator writeCoordinator,
PackerEventSink eventSink) { PackerEventSink eventSink) {
this.mapper = Objects.requireNonNull(mapper, "mapper"); this.mapper = Objects.requireNonNull(mapper, "mapper");
@ -46,6 +49,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
this.actionReadService = Objects.requireNonNull(actionReadService, "actionReadService"); this.actionReadService = Objects.requireNonNull(actionReadService, "actionReadService");
this.runtimePatchService = Objects.requireNonNull(runtimePatchService, "runtimePatchService"); this.runtimePatchService = Objects.requireNonNull(runtimePatchService, "runtimePatchService");
this.runtimeRegistry = Objects.requireNonNull(runtimeRegistry, "runtimeRegistry"); this.runtimeRegistry = Objects.requireNonNull(runtimeRegistry, "runtimeRegistry");
this.cacheRepository = Objects.requireNonNull(cacheRepository, "cacheRepository");
this.writeCoordinator = Objects.requireNonNull(writeCoordinator, "writeCoordinator"); this.writeCoordinator = Objects.requireNonNull(writeCoordinator, "writeCoordinator");
this.eventSink = Objects.requireNonNull(eventSink, "eventSink"); this.eventSink = Objects.requireNonNull(eventSink, "eventSink");
} }
@ -84,6 +88,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
final var registryEntry = registryByRoot.get(assetRoot); final var registryEntry = registryByRoot.get(assetRoot);
final var parsed = runtimeAsset.parsedDeclaration(); final var parsed = runtimeAsset.parsedDeclaration();
diagnostics.addAll(parsed.diagnostics()); diagnostics.addAll(parsed.diagnostics());
diagnostics.addAll(runtimeAsset.walkDiagnostics());
diagnostics.addAll(identityMismatchDiagnostics(registryEntry, parsed, assetManifestPath)); diagnostics.addAll(identityMismatchDiagnostics(registryEntry, parsed, assetManifestPath));
final var summary = buildSummary(project, assetRoot, registryEntry, parsed); final var summary = buildSummary(project, assetRoot, registryEntry, parsed);
assets.add(summary); assets.add(summary);
@ -220,13 +225,14 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
writeManifest(manifestPath, request, entry.assetUuid()); writeManifest(manifestPath, request, entry.assetUuid());
final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry); final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry);
workspaceFoundation.saveRegistry(project, updated); workspaceFoundation.saveRegistry(project, updated);
runtimeRegistry.update(project, (snapshot, generation) -> runtimePatchService.afterCreateAsset( final var runtime = runtimeRegistry.update(project, (snapshot, generation) -> runtimePatchService.afterCreateAsset(
snapshot, snapshot,
generation, generation,
updated, updated,
entry, entry,
assetRoot, assetRoot,
manifestPath)); manifestPath));
saveRuntimeCache(project, runtime.snapshot());
final CreateAssetResult result = new CreateAssetResult( final CreateAssetResult result = new CreateAssetResult(
PackerOperationStatus.SUCCESS, PackerOperationStatus.SUCCESS,
"Asset created: " + relativeAssetRoot, "Asset created: " + relativeAssetRoot,
@ -270,12 +276,13 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
declaration.assetUuid()); declaration.assetUuid());
final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry); final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry);
workspaceFoundation.saveRegistry(project, updated); workspaceFoundation.saveRegistry(project, updated);
runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterRegisterAsset( final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterRegisterAsset(
currentSnapshot, currentSnapshot,
generation, generation,
updated, updated,
entry, entry,
assetRoot)); assetRoot));
saveRuntimeCache(project, runtime.snapshot());
final RegisterAssetResult result = new RegisterAssetResult( final RegisterAssetResult result = new RegisterAssetResult(
PackerOperationStatus.SUCCESS, PackerOperationStatus.SUCCESS,
"Asset registered: " + relativeAssetRoot, "Asset registered: " + relativeAssetRoot,
@ -316,11 +323,12 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
if (!updatedRegistry.equals(registry)) { if (!updatedRegistry.equals(registry)) {
workspaceFoundation.saveRegistry(project, updatedRegistry); workspaceFoundation.saveRegistry(project, updatedRegistry);
} }
runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterDeleteAsset( final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterDeleteAsset(
currentSnapshot, currentSnapshot,
generation, generation,
updatedRegistry, updatedRegistry,
assetRoot)); assetRoot));
saveRuntimeCache(project, runtime.snapshot());
final DeleteAssetResult result = new DeleteAssetResult( final DeleteAssetResult result = new DeleteAssetResult(
PackerOperationStatus.SUCCESS, PackerOperationStatus.SUCCESS,
"Asset deleted: " + relativeAssetRoot, "Asset deleted: " + relativeAssetRoot,
@ -372,7 +380,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
if (!updatedRegistry.equals(registry)) { if (!updatedRegistry.equals(registry)) {
workspaceFoundation.saveRegistry(project, updatedRegistry); workspaceFoundation.saveRegistry(project, updatedRegistry);
} }
runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterMoveAsset( final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterMoveAsset(
currentSnapshot, currentSnapshot,
generation, generation,
updatedRegistry, updatedRegistry,
@ -380,6 +388,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
sourceRoot, sourceRoot,
targetRoot, targetRoot,
targetManifestPath)); targetManifestPath));
saveRuntimeCache(project, runtime.snapshot());
final AssetReference canonicalReference = updatedEntry final AssetReference canonicalReference = updatedEntry
.map(packerRegistryEntry -> AssetReference.forAssetId(packerRegistryEntry.assetId())) .map(packerRegistryEntry -> AssetReference.forAssetId(packerRegistryEntry.assetId()))
.orElseGet(() -> AssetReference.forRelativeAssetRoot(targetRelativeRoot)); .orElseGet(() -> AssetReference.forRelativeAssetRoot(targetRelativeRoot));
@ -558,6 +567,10 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
return PackerWorkspacePaths.relativeAssetRoot(project, assetRoot).replace('\\', '/'); return PackerWorkspacePaths.relativeAssetRoot(project, assetRoot).replace('\\', '/');
} }
private void saveRuntimeCache(PackerProjectContext project, PackerRuntimeSnapshot snapshot) {
cacheRepository.save(project, snapshot.cacheState());
}
private PackerRegistryState removeRegistryEntry( private PackerRegistryState removeRegistryEntry(
PackerRegistryState registry, PackerRegistryState registry,
PackerRegistryEntry entry) { PackerRegistryEntry entry) {
@ -628,12 +641,13 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
try { try {
patchManifestContract(manifest, request); patchManifestContract(manifest, request);
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest); mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterUpdateAssetContract( final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterUpdateAssetContract(
currentSnapshot, currentSnapshot,
generation, generation,
assetRoot, assetRoot,
manifestPath, manifestPath,
evaluation.resolved().registryEntry())); evaluation.resolved().registryEntry()));
saveRuntimeCache(project, runtime.snapshot());
return new UpdateAssetContractResponse(true, null); return new UpdateAssetContractResponse(true, null);
} catch (IOException exception) { } catch (IOException exception) {
return new UpdateAssetContractResponse(false, "Unable to update asset contract: " + exception.getMessage()); return new UpdateAssetContractResponse(false, "Unable to update asset contract: " + exception.getMessage());

View File

@ -42,6 +42,7 @@ public final class PackerAssetDetailsService {
final var manifestPath = runtimeAsset.manifestPath(); final var manifestPath = runtimeAsset.manifestPath();
final var parsed = runtimeAsset.parsedDeclaration(); final var parsed = runtimeAsset.parsedDeclaration();
diagnostics.addAll(parsed.diagnostics()); diagnostics.addAll(parsed.diagnostics());
diagnostics.addAll(runtimeAsset.walkDiagnostics());
if (!parsed.valid()) { if (!parsed.valid()) {
return failureResult(project, request.assetReference(), resolved, diagnostics); return failureResult(project, request.assetReference(), resolved, diagnostics);
} }

View File

@ -1,15 +1,21 @@
package p.packer.services; package p.packer.services;
import p.packer.models.*; import p.packer.models.*;
import p.packer.repositories.PackerRuntimeAssetMaterializer;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
public final class PackerRuntimePatchService { public final class PackerRuntimePatchService {
private final PackerAssetDeclarationParser declarationParser; private final PackerAssetDeclarationParser declarationParser;
private final PackerRuntimeAssetMaterializer assetMaterializer;
public PackerRuntimePatchService(PackerAssetDeclarationParser declarationParser) { public PackerRuntimePatchService(
PackerAssetDeclarationParser declarationParser,
PackerRuntimeAssetMaterializer assetMaterializer) {
this.declarationParser = Objects.requireNonNull(declarationParser, "declarationParser"); this.declarationParser = Objects.requireNonNull(declarationParser, "declarationParser");
this.assetMaterializer = Objects.requireNonNull(assetMaterializer, "assetMaterializer");
} }
public PackerRuntimeSnapshot afterCreateAsset( public PackerRuntimeSnapshot afterCreateAsset(
@ -23,13 +29,19 @@ public final class PackerRuntimePatchService {
final List<PackerRuntimeAsset> updatedAssets = new ArrayList<>(snapshot.assets().stream() final List<PackerRuntimeAsset> updatedAssets = new ArrayList<>(snapshot.assets().stream()
.filter(candidate -> !candidate.assetRoot().equals(assetRoot.toAbsolutePath().normalize())) .filter(candidate -> !candidate.assetRoot().equals(assetRoot.toAbsolutePath().normalize()))
.toList()); .toList());
updatedAssets.add(new PackerRuntimeAsset( final var materialized = assetMaterializer.materialize(
assetRoot, assetRoot,
manifestPath, manifestPath,
Optional.of(entry), Optional.of(entry),
parsed)); parsed,
snapshot.cacheState().findAsset(entry.assetId()));
updatedAssets.add(materialized.runtimeAsset());
updatedAssets.sort(Comparator.comparing(asset -> asset.assetRoot().toString(), String.CASE_INSENSITIVE_ORDER)); updatedAssets.sort(Comparator.comparing(asset -> asset.assetRoot().toString(), String.CASE_INSENSITIVE_ORDER));
return new PackerRuntimeSnapshot(generation, updatedRegistry, updatedAssets); return new PackerRuntimeSnapshot(
generation,
updatedRegistry,
updatedAssets,
mergeCacheState(snapshot.cacheState(), updatedRegistry, materialized.assetCacheEntry()));
} }
public PackerRuntimeSnapshot afterRegisterAsset( public PackerRuntimeSnapshot afterRegisterAsset(
@ -40,13 +52,17 @@ public final class PackerRuntimePatchService {
Path assetRoot) { Path assetRoot) {
final List<PackerRuntimeAsset> updatedAssets = new ArrayList<>(); final List<PackerRuntimeAsset> updatedAssets = new ArrayList<>();
boolean patched = false; boolean patched = false;
Optional<PackerAssetCacheEntry> refreshedCacheEntry = Optional.empty();
for (PackerRuntimeAsset asset : snapshot.assets()) { for (PackerRuntimeAsset asset : snapshot.assets()) {
if (asset.assetRoot().equals(assetRoot.toAbsolutePath().normalize())) { if (asset.assetRoot().equals(assetRoot.toAbsolutePath().normalize())) {
updatedAssets.add(new PackerRuntimeAsset( final var materialized = assetMaterializer.materialize(
asset.assetRoot(), asset.assetRoot(),
asset.manifestPath(), asset.manifestPath(),
Optional.of(entry), Optional.of(entry),
asset.parsedDeclaration())); asset.parsedDeclaration(),
snapshot.cacheState().findAsset(entry.assetId()));
updatedAssets.add(materialized.runtimeAsset());
refreshedCacheEntry = materialized.assetCacheEntry();
patched = true; patched = true;
} else { } else {
updatedAssets.add(asset); updatedAssets.add(asset);
@ -55,7 +71,11 @@ public final class PackerRuntimePatchService {
if (!patched) { if (!patched) {
throw new IllegalStateException("Unable to patch runtime snapshot for unregistered asset: " + assetRoot); throw new IllegalStateException("Unable to patch runtime snapshot for unregistered asset: " + assetRoot);
} }
return new PackerRuntimeSnapshot(generation, updatedRegistry, updatedAssets); return new PackerRuntimeSnapshot(
generation,
updatedRegistry,
updatedAssets,
mergeCacheState(snapshot.cacheState(), updatedRegistry, refreshedCacheEntry));
} }
public PackerRuntimeSnapshot afterDeleteAsset( public PackerRuntimeSnapshot afterDeleteAsset(
@ -66,7 +86,11 @@ public final class PackerRuntimePatchService {
final List<PackerRuntimeAsset> updatedAssets = snapshot.assets().stream() final List<PackerRuntimeAsset> updatedAssets = snapshot.assets().stream()
.filter(asset -> !asset.assetRoot().equals(assetRoot.toAbsolutePath().normalize())) .filter(asset -> !asset.assetRoot().equals(assetRoot.toAbsolutePath().normalize()))
.toList(); .toList();
return new PackerRuntimeSnapshot(generation, updatedRegistry, updatedAssets); return new PackerRuntimeSnapshot(
generation,
updatedRegistry,
updatedAssets,
pruneCacheState(snapshot.cacheState(), updatedRegistry));
} }
public PackerRuntimeSnapshot afterMoveAsset( public PackerRuntimeSnapshot afterMoveAsset(
@ -80,13 +104,17 @@ public final class PackerRuntimePatchService {
final PackerAssetDeclarationParseResult parsed = declarationParser.parse(targetManifestPath); final PackerAssetDeclarationParseResult parsed = declarationParser.parse(targetManifestPath);
final List<PackerRuntimeAsset> updatedAssets = new ArrayList<>(); final List<PackerRuntimeAsset> updatedAssets = new ArrayList<>();
boolean patched = false; boolean patched = false;
Optional<PackerAssetCacheEntry> refreshedCacheEntry = Optional.empty();
for (PackerRuntimeAsset asset : snapshot.assets()) { for (PackerRuntimeAsset asset : snapshot.assets()) {
if (asset.assetRoot().equals(sourceRoot.toAbsolutePath().normalize())) { if (asset.assetRoot().equals(sourceRoot.toAbsolutePath().normalize())) {
updatedAssets.add(new PackerRuntimeAsset( final var materialized = assetMaterializer.materialize(
targetRoot, targetRoot,
targetManifestPath, targetManifestPath,
updatedRegistryEntry, updatedRegistryEntry,
parsed)); parsed,
updatedRegistryEntry.flatMap(entry -> snapshot.cacheState().findAsset(entry.assetId())));
updatedAssets.add(materialized.runtimeAsset());
refreshedCacheEntry = materialized.assetCacheEntry();
patched = true; patched = true;
} else { } else {
updatedAssets.add(asset); updatedAssets.add(asset);
@ -96,7 +124,11 @@ public final class PackerRuntimePatchService {
throw new IllegalStateException("Unable to patch runtime snapshot for moved asset: " + sourceRoot); throw new IllegalStateException("Unable to patch runtime snapshot for moved asset: " + sourceRoot);
} }
updatedAssets.sort(Comparator.comparing(asset -> asset.assetRoot().toString(), String.CASE_INSENSITIVE_ORDER)); updatedAssets.sort(Comparator.comparing(asset -> asset.assetRoot().toString(), String.CASE_INSENSITIVE_ORDER));
return new PackerRuntimeSnapshot(generation, updatedRegistry, updatedAssets); return new PackerRuntimeSnapshot(
generation,
updatedRegistry,
updatedAssets,
mergeCacheState(snapshot.cacheState(), updatedRegistry, refreshedCacheEntry));
} }
public PackerRuntimeSnapshot afterUpdateAssetContract( public PackerRuntimeSnapshot afterUpdateAssetContract(
@ -108,26 +140,69 @@ public final class PackerRuntimePatchService {
final PackerAssetDeclarationParseResult parsed = declarationParser.parse(manifestPath); final PackerAssetDeclarationParseResult parsed = declarationParser.parse(manifestPath);
final List<PackerRuntimeAsset> updatedAssets = new ArrayList<>(); final List<PackerRuntimeAsset> updatedAssets = new ArrayList<>();
boolean patched = false; boolean patched = false;
Optional<PackerAssetCacheEntry> refreshedCacheEntry = Optional.empty();
for (PackerRuntimeAsset asset : snapshot.assets()) { for (PackerRuntimeAsset asset : snapshot.assets()) {
if (asset.assetRoot().equals(assetRoot.toAbsolutePath().normalize())) { if (asset.assetRoot().equals(assetRoot.toAbsolutePath().normalize())) {
updatedAssets.add(new PackerRuntimeAsset( final var materialized = assetMaterializer.materialize(
asset.assetRoot(), asset.assetRoot(),
asset.manifestPath(), asset.manifestPath(),
asset.registryEntry(), asset.registryEntry(),
parsed)); parsed,
asset.registryEntry().flatMap(entry -> snapshot.cacheState().findAsset(entry.assetId())));
updatedAssets.add(materialized.runtimeAsset());
refreshedCacheEntry = materialized.assetCacheEntry();
patched = true; patched = true;
} else { } else {
updatedAssets.add(asset); updatedAssets.add(asset);
} }
} }
if (!patched) { if (!patched) {
updatedAssets.add(new PackerRuntimeAsset( final var materialized = assetMaterializer.materialize(
assetRoot, assetRoot,
manifestPath, manifestPath,
registryEntry, registryEntry,
parsed)); parsed,
registryEntry.flatMap(entry -> snapshot.cacheState().findAsset(entry.assetId())));
updatedAssets.add(materialized.runtimeAsset());
refreshedCacheEntry = materialized.assetCacheEntry();
updatedAssets.sort(Comparator.comparing(asset -> asset.assetRoot().toString(), String.CASE_INSENSITIVE_ORDER)); updatedAssets.sort(Comparator.comparing(asset -> asset.assetRoot().toString(), String.CASE_INSENSITIVE_ORDER));
} }
return new PackerRuntimeSnapshot(generation, snapshot.registry(), updatedAssets); return new PackerRuntimeSnapshot(
generation,
snapshot.registry(),
updatedAssets,
mergeCacheState(snapshot.cacheState(), snapshot.registry(), refreshedCacheEntry));
}
private PackerWorkspaceCacheState mergeCacheState(
PackerWorkspaceCacheState currentCacheState,
PackerRegistryState registry,
Optional<PackerAssetCacheEntry> refreshedCacheEntry) {
final Map<Integer, PackerAssetCacheEntry> byAssetId = new LinkedHashMap<>();
currentCacheState.assets().forEach(entry -> byAssetId.put(entry.assetId(), entry));
refreshedCacheEntry.ifPresent(entry -> byAssetId.put(entry.assetId(), entry));
return pruneCacheEntries(byAssetId, registry);
}
private PackerWorkspaceCacheState pruneCacheState(
PackerWorkspaceCacheState currentCacheState,
PackerRegistryState registry) {
final Map<Integer, PackerAssetCacheEntry> byAssetId = new LinkedHashMap<>();
currentCacheState.assets().forEach(entry -> byAssetId.put(entry.assetId(), entry));
return pruneCacheEntries(byAssetId, registry);
}
private PackerWorkspaceCacheState pruneCacheEntries(
Map<Integer, PackerAssetCacheEntry> byAssetId,
PackerRegistryState registry) {
final Set<Integer> activeAssetIds = registry.assets().stream()
.map(PackerRegistryEntry::assetId)
.collect(Collectors.toSet());
return new PackerWorkspaceCacheState(
PackerWorkspaceCacheState.CURRENT_SCHEMA_VERSION,
byAssetId.values().stream()
.filter(entry -> activeAssetIds.contains(entry.assetId()))
.sorted(Comparator.comparingInt(PackerAssetCacheEntry::assetId))
.toList());
} }
} }

View File

@ -7,7 +7,9 @@ import p.packer.events.PackerEvent;
import p.packer.events.PackerEventKind; import p.packer.events.PackerEventKind;
import p.packer.messages.*; import p.packer.messages.*;
import p.packer.messages.assets.*; import p.packer.messages.assets.*;
import p.packer.repositories.FileSystemPackerCacheRepository;
import p.packer.repositories.PackerAssetWalker; import p.packer.repositories.PackerAssetWalker;
import p.packer.repositories.PackerRuntimeAssetMaterializer;
import p.packer.repositories.PackerRuntimeLoader; import p.packer.repositories.PackerRuntimeLoader;
import p.packer.repositories.PackerRuntimeRegistry; import p.packer.repositories.PackerRuntimeRegistry;
import p.packer.repositories.PackerRuntimeSnapshotLoader; import p.packer.repositories.PackerRuntimeSnapshotLoader;
@ -138,7 +140,8 @@ final class FileSystemPackerWorkspaceServiceTest {
assertEquals(PackerAssetState.REGISTERED, detailsResult.details().summary().state()); assertEquals(PackerAssetState.REGISTERED, detailsResult.details().summary().state());
assertEquals("new_atlas", detailsResult.details().summary().identity().assetName()); assertEquals("new_atlas", detailsResult.details().summary().identity().assetName());
assertNotNull(detailsResult.details().summary().identity().assetUuid()); assertNotNull(detailsResult.details().summary().identity().assetUuid());
assertTrue(detailsResult.diagnostics().isEmpty()); assertTrue(detailsResult.diagnostics().stream().noneMatch(diagnostic -> diagnostic.blocking()));
assertTrue(detailsResult.diagnostics().stream().anyMatch(diagnostic -> diagnostic.message().contains("Output metadata for tile bank cannot be empty")));
} }
@Test @Test
@ -679,11 +682,14 @@ final class FileSystemPackerWorkspaceServiceTest {
final var mapper = new ObjectMapper(); final var mapper = new ObjectMapper();
final var foundation = new p.packer.services.PackerWorkspaceFoundation(mapper); final var foundation = new p.packer.services.PackerWorkspaceFoundation(mapper);
final var parser = new p.packer.services.PackerAssetDeclarationParser(mapper); final var parser = new p.packer.services.PackerAssetDeclarationParser(mapper);
final var assetWalker = new PackerAssetWalker(mapper);
final var cacheRepository = new FileSystemPackerCacheRepository(mapper);
final var assetMaterializer = new PackerRuntimeAssetMaterializer(assetWalker);
final var runtimeRegistry = new PackerRuntimeRegistry(loader); final var runtimeRegistry = new PackerRuntimeRegistry(loader);
final var resolver = new p.packer.services.PackerAssetReferenceResolver(foundation.lookup()); final var resolver = new p.packer.services.PackerAssetReferenceResolver(foundation.lookup());
final var detailsService = new p.packer.services.PackerAssetDetailsService(runtimeRegistry, resolver); final var detailsService = new p.packer.services.PackerAssetDetailsService(runtimeRegistry, resolver);
final var actionReadService = new p.packer.services.PackerAssetActionReadService(runtimeRegistry, resolver, foundation.lookup()); final var actionReadService = new p.packer.services.PackerAssetActionReadService(runtimeRegistry, resolver, foundation.lookup());
final var runtimePatchService = new p.packer.services.PackerRuntimePatchService(parser); final var runtimePatchService = new p.packer.services.PackerRuntimePatchService(parser, assetMaterializer);
final var writeCoordinator = new p.packer.services.PackerProjectWriteCoordinator(); final var writeCoordinator = new p.packer.services.PackerProjectWriteCoordinator();
return new FileSystemPackerWorkspaceService( return new FileSystemPackerWorkspaceService(
new ObjectMapper(), new ObjectMapper(),
@ -692,6 +698,7 @@ final class FileSystemPackerWorkspaceServiceTest {
actionReadService, actionReadService,
runtimePatchService, runtimePatchService,
runtimeRegistry, runtimeRegistry,
cacheRepository,
writeCoordinator, writeCoordinator,
eventSink); eventSink);
} }
@ -701,7 +708,9 @@ final class FileSystemPackerWorkspaceServiceTest {
final var foundation = new p.packer.services.PackerWorkspaceFoundation(mapper); final var foundation = new p.packer.services.PackerWorkspaceFoundation(mapper);
final var parser = new p.packer.services.PackerAssetDeclarationParser(mapper); final var parser = new p.packer.services.PackerAssetDeclarationParser(mapper);
final var assetWalker = new PackerAssetWalker(mapper); final var assetWalker = new PackerAssetWalker(mapper);
return new CountingLoader(new PackerRuntimeLoader(foundation, parser, assetWalker)); final var cacheRepository = new FileSystemPackerCacheRepository(mapper);
final var assetMaterializer = new PackerRuntimeAssetMaterializer(assetWalker);
return new CountingLoader(new PackerRuntimeLoader(foundation, parser, cacheRepository, assetMaterializer));
} }
private static final class CountingLoader implements PackerRuntimeSnapshotLoader { private static final class CountingLoader implements PackerRuntimeSnapshotLoader {

View File

@ -11,7 +11,9 @@ import p.packer.messages.PackerProjectContext;
import p.packer.messages.assets.OutputCodecCatalog; import p.packer.messages.assets.OutputCodecCatalog;
import p.packer.messages.assets.PackerAssetState; import p.packer.messages.assets.PackerAssetState;
import p.packer.messages.assets.PackerBuildParticipation; import p.packer.messages.assets.PackerBuildParticipation;
import p.packer.repositories.FileSystemPackerCacheRepository;
import p.packer.repositories.PackerAssetWalker; import p.packer.repositories.PackerAssetWalker;
import p.packer.repositories.PackerRuntimeAssetMaterializer;
import p.packer.repositories.PackerRuntimeLoader; import p.packer.repositories.PackerRuntimeLoader;
import p.packer.repositories.PackerRuntimeRegistry; import p.packer.repositories.PackerRuntimeRegistry;
import p.packer.testing.PackerFixtureLocator; import p.packer.testing.PackerFixtureLocator;
@ -43,7 +45,8 @@ final class PackerAssetDetailsServiceTest {
assertEquals("TILES/indexed_v1", result.details().outputFormat().displayName()); assertEquals("TILES/indexed_v1", result.details().outputFormat().displayName());
assertEquals(List.of(OutputCodecCatalog.NONE), result.details().availableOutputCodecs()); assertEquals(List.of(OutputCodecCatalog.NONE), result.details().availableOutputCodecs());
assertEquals(List.of(), result.details().codecConfigurationFieldsByCodec().get(OutputCodecCatalog.NONE)); assertEquals(List.of(), result.details().codecConfigurationFieldsByCodec().get(OutputCodecCatalog.NONE));
assertTrue(result.diagnostics().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 @Test
@ -137,7 +140,13 @@ final class PackerAssetDetailsServiceTest {
final var foundation = new PackerWorkspaceFoundation(mapper); final var foundation = new PackerWorkspaceFoundation(mapper);
final var parser = new PackerAssetDeclarationParser(mapper); final var parser = new PackerAssetDeclarationParser(mapper);
final var assetWalker = new PackerAssetWalker(mapper); final var assetWalker = new PackerAssetWalker(mapper);
final var runtimeRegistry = new PackerRuntimeRegistry(new PackerRuntimeLoader(foundation, parser, assetWalker)); final var cacheRepository = new FileSystemPackerCacheRepository(mapper);
final var assetMaterializer = new PackerRuntimeAssetMaterializer(assetWalker);
final var runtimeRegistry = new PackerRuntimeRegistry(new PackerRuntimeLoader(
foundation,
parser,
cacheRepository,
assetMaterializer));
final var resolver = new PackerAssetReferenceResolver(foundation.lookup()); final var resolver = new PackerAssetReferenceResolver(foundation.lookup());
return new PackerAssetDetailsService(runtimeRegistry, resolver); return new PackerAssetDetailsService(runtimeRegistry, resolver);
} }

View File

@ -0,0 +1,80 @@
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.PackerProjectContext;
import p.packer.repositories.FileSystemPackerCacheRepository;
import p.packer.repositories.PackerAssetWalker;
import p.packer.repositories.PackerRuntimeAssetMaterializer;
import p.packer.repositories.PackerRuntimeLoader;
import p.packer.testing.PackerFixtureLocator;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
final class PackerRuntimePatchServiceTest {
@TempDir
Path tempDir;
@Test
void afterUpdateAssetContractRefreshesOneAssetProjectionAndCacheState() throws Exception {
final ObjectMapper mapper = new ObjectMapper();
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("managed"));
writeTilePng(projectRoot.resolve("assets/ui/atlas/tile.png"));
final var foundation = new PackerWorkspaceFoundation(mapper);
final var parser = new PackerAssetDeclarationParser(mapper);
final var assetWalker = new PackerAssetWalker(mapper);
final var cacheRepository = new FileSystemPackerCacheRepository(mapper);
final var assetMaterializer = new PackerRuntimeAssetMaterializer(assetWalker);
final var loader = new PackerRuntimeLoader(foundation, parser, cacheRepository, assetMaterializer);
final var patchService = new PackerRuntimePatchService(parser, assetMaterializer);
final var project = new PackerProjectContext("main", projectRoot);
final var initialSnapshot = loader.load(project, 1L);
final var registryEntry = initialSnapshot.registry().assets().getFirst();
final var updatedSnapshot = patchService.afterUpdateAssetContract(
initialSnapshot,
2L,
projectRoot.resolve("assets/ui/atlas"),
projectRoot.resolve("assets/ui/atlas/asset.json"),
Optional.of(registryEntry));
assertTrue(updatedSnapshot.cacheState().findAsset(registryEntry.assetId()).isPresent());
assertEquals(1, updatedSnapshot.assets().getFirst().walkProjection().buildCandidateFiles().size());
assertEquals("tile.png", updatedSnapshot.assets().getFirst().walkProjection().buildCandidateFiles().getFirst().relativePath());
}
private Path copyFixture(String relativePath, Path targetRoot) throws Exception {
final Path sourceRoot = PackerFixtureLocator.fixtureRoot(relativePath);
try (var stream = Files.walk(sourceRoot)) {
for (Path source : stream.sorted(Comparator.naturalOrder()).toList()) {
final Path target = targetRoot.resolve(sourceRoot.relativize(source).toString());
if (Files.isDirectory(source)) {
Files.createDirectories(target);
} else {
Files.createDirectories(target.getParent());
Files.copy(source, target);
}
}
}
return targetRoot;
}
private void writeTilePng(Path path) throws Exception {
Files.createDirectories(path.getParent());
final BufferedImage image = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < 16; y += 1) {
for (int x = 0; x < 16; x += 1) {
image.setRGB(x, y, 0xFFFF0000);
}
}
ImageIO.write(image, "png", path.toFile());
}
}

View File

@ -3,15 +3,21 @@ package p.packer.services;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
import p.packer.PackerWorkspacePaths;
import p.packer.messages.PackerProjectContext; import p.packer.messages.PackerProjectContext;
import p.packer.repositories.FileSystemPackerCacheRepository;
import p.packer.repositories.PackerAssetWalker; import p.packer.repositories.PackerAssetWalker;
import p.packer.repositories.PackerRuntimeAssetMaterializer;
import p.packer.repositories.PackerRuntimeLoader; import p.packer.repositories.PackerRuntimeLoader;
import p.packer.repositories.PackerRuntimeRegistry; import p.packer.repositories.PackerRuntimeRegistry;
import p.packer.testing.PackerFixtureLocator; import p.packer.testing.PackerFixtureLocator;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Comparator; import java.util.Comparator;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@ -66,6 +72,24 @@ final class PackerRuntimeRegistryTest {
assertEquals(2, refreshed.snapshot().assets().size()); assertEquals(2, refreshed.snapshot().assets().size());
} }
@Test
void runtimeSnapshotRetainsWalkProjectionAndPersistsCacheForRegisteredAssets() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("cache"));
writeTilePng(projectRoot.resolve("assets/ui/atlas/tile.png"));
final PackerRuntimeRegistry registry = runtimeRegistry();
final PackerProjectContext project = project(projectRoot);
final PackerProjectRuntime runtime = registry.getOrLoad(project);
assertTrue(Files.isRegularFile(PackerWorkspacePaths.cachePath(project)));
assertTrue(runtime.snapshot().cacheState().findAsset(1).isPresent());
final var runtimeAsset = runtime.snapshot().assets().getFirst();
assertEquals(List.of("tile.png"), runtimeAsset.walkProjection().availableFiles());
assertEquals(1, runtimeAsset.walkProjection().buildCandidateFiles().size());
assertEquals("tile.png", runtimeAsset.walkProjection().buildCandidateFiles().getFirst().relativePath());
assertTrue(runtimeAsset.walkProjection().measuredBankSizeBytes() > 0L);
}
@Test @Test
void disposeMarksRuntimeInactiveAndRemovesItFromRegistry() throws Exception { void disposeMarksRuntimeInactiveAndRemovesItFromRegistry() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("dispose")); final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("dispose"));
@ -85,7 +109,13 @@ final class PackerRuntimeRegistryTest {
final var foundation = new PackerWorkspaceFoundation(mapper); final var foundation = new PackerWorkspaceFoundation(mapper);
final var parser = new PackerAssetDeclarationParser(mapper); final var parser = new PackerAssetDeclarationParser(mapper);
final var assetWalker = new PackerAssetWalker(mapper); final var assetWalker = new PackerAssetWalker(mapper);
return new PackerRuntimeRegistry(new PackerRuntimeLoader(foundation, parser, assetWalker)); final var cacheRepository = new FileSystemPackerCacheRepository(mapper);
final var assetMaterializer = new PackerRuntimeAssetMaterializer(assetWalker);
return new PackerRuntimeRegistry(new PackerRuntimeLoader(
foundation,
parser,
cacheRepository,
assetMaterializer));
} }
private PackerProjectContext project(Path root) { private PackerProjectContext project(Path root) {
@ -107,4 +137,15 @@ final class PackerRuntimeRegistryTest {
} }
return targetRoot; return targetRoot;
} }
private void writeTilePng(Path path) throws Exception {
Files.createDirectories(path.getParent());
final BufferedImage image = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < 16; y += 1) {
for (int x = 0; x < 16; x += 1) {
image.setRGB(x, y, 0xFFFF0000);
}
}
ImageIO.write(image, "png", path.toFile());
}
} }