implements PLN-0053

This commit is contained in:
bQUARKz 2026-04-18 17:46:04 +01:00
parent 20a23dedb5
commit bcc5b413fa
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
26 changed files with 498 additions and 5 deletions

View File

@ -4,6 +4,7 @@ import java.util.Locale;
public enum AssetFamilyCatalog { public enum AssetFamilyCatalog {
GLYPH_BANK("glyph_bank"), GLYPH_BANK("glyph_bank"),
SCENE_BANK("scene_bank"),
SOUND_BANK("sound_bank"), SOUND_BANK("sound_bank"),
UNKNOWN("unknown"); UNKNOWN("unknown");

View File

@ -5,6 +5,7 @@ import java.util.Locale;
public enum OutputFormatCatalog { public enum OutputFormatCatalog {
GLYPH_INDEXED_V1(AssetFamilyCatalog.GLYPH_BANK, "GLYPH/indexed_v1", "GLYPH/indexed_v1"), GLYPH_INDEXED_V1(AssetFamilyCatalog.GLYPH_BANK, "GLYPH/indexed_v1", "GLYPH/indexed_v1"),
SCENE_TILED_V1(AssetFamilyCatalog.SCENE_BANK, "SCENE/tiled_v1", "SCENE/tiled_v1"),
SOUND_V1(AssetFamilyCatalog.SOUND_BANK, "SOUND/v1", "SOUND/v1"), SOUND_V1(AssetFamilyCatalog.SOUND_BANK, "SOUND/v1", "SOUND/v1"),
UNKNOWN(AssetFamilyCatalog.UNKNOWN, "unknown", "Unknown"); UNKNOWN(AssetFamilyCatalog.UNKNOWN, "unknown", "Unknown");

View File

@ -100,6 +100,9 @@ public class PackerAssetWalker {
diagnostics.addAll(walkResult.diagnostics()); diagnostics.addAll(walkResult.diagnostics());
return new PackerWalkResult(walkResult.probeResults(), diagnostics); return new PackerWalkResult(walkResult.probeResults(), diagnostics);
} }
case SCENE_BANK -> {
return new PackerWalkResult(List.of(), diagnostics);
}
case UNKNOWN -> { case UNKNOWN -> {
diagnostics.add(new PackerDiagnostic( diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.WARNING, PackerDiagnosticSeverity.WARNING,

View File

@ -38,6 +38,7 @@ import java.util.stream.Stream;
public final class FileSystemPackerWorkspaceService implements PackerWorkspaceService { public final class FileSystemPackerWorkspaceService implements PackerWorkspaceService {
private static final int GLYPH_BANK_COLOR_KEY_RGB565 = 0xF81F; private static final int GLYPH_BANK_COLOR_KEY_RGB565 = 0xF81F;
private static final String SCENE_BANK_SUPPORT_FILE = "scene-bank.studio.json";
private final ObjectMapper mapper; private final ObjectMapper mapper;
private final PackerWorkspaceFoundation workspaceFoundation; private final PackerWorkspaceFoundation workspaceFoundation;
@ -287,6 +288,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
final PackerRegistryEntry entry = workspaceFoundation.allocateIdentity(project, registry, assetRoot); final PackerRegistryEntry entry = workspaceFoundation.allocateIdentity(project, registry, assetRoot);
Files.createDirectories(assetRoot); Files.createDirectories(assetRoot);
writeManifest(manifestPath, request, entry.assetUuid()); writeManifest(manifestPath, request, entry.assetUuid());
writeStudioSupportFiles(assetRoot, request);
final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry); final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry);
workspaceFoundation.saveRegistry(project, updated); workspaceFoundation.saveRegistry(project, updated);
final var runtime = runtimeRegistry.update(project, (snapshot, generation) -> runtimePatchService.afterCreateAsset( final var runtime = runtimeRegistry.update(project, (snapshot, generation) -> runtimePatchService.afterCreateAsset(
@ -667,6 +669,21 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest); mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
} }
private void writeStudioSupportFiles(Path assetRoot, CreateAssetRequest request) throws IOException {
if (request.assetFamily() != AssetFamilyCatalog.SCENE_BANK) {
return;
}
final Map<String, Object> supportFile = new LinkedHashMap<>();
supportFile.put("schema_version", 1);
supportFile.put("layer_count", 1);
supportFile.put("layers", List.of(Map.of(
"index", 1,
"tilemap", "layer-1.tmx")));
mapper.writerWithDefaultPrettyPrinter().writeValue(
assetRoot.resolve(SCENE_BANK_SUPPORT_FILE).toFile(),
supportFile);
}
private String normalizeRelativeAssetRoot(String candidate) { private String normalizeRelativeAssetRoot(String candidate) {
final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/'); final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/');
if (raw.isBlank()) { if (raw.isBlank()) {

View File

@ -121,7 +121,7 @@ public final class PackerAssetDeclarationParser {
diagnostics.add(new PackerDiagnostic( diagnostics.add(new PackerDiagnostic(
PackerDiagnosticSeverity.ERROR, PackerDiagnosticSeverity.ERROR,
PackerDiagnosticCategory.STRUCTURAL, PackerDiagnosticCategory.STRUCTURAL,
"Field 'type' must be one of: glyph_bank, palette_bank, sound_bank.", "Field 'type' must be one of: glyph_bank, scene_bank, sound_bank.",
manifestPath, manifestPath,
true)); true));
return null; return null;

View File

@ -512,6 +512,29 @@ final class FileSystemPackerWorkspaceServiceTest {
assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.ACTION_APPLIED)); assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.ACTION_APPLIED));
} }
@Test
void createsSceneBankAssetAndWritesStudioSupportFile() throws Exception {
final Path projectRoot = tempDir.resolve("created-scene-bank");
final FileSystemPackerWorkspaceService service = service();
final var result = service.createAsset(new CreateAssetRequest(
project(projectRoot),
"scenes/overworld",
"overworld",
AssetFamilyCatalog.SCENE_BANK,
OutputFormatCatalog.SCENE_TILED_V1,
OutputCodecCatalog.NONE,
false));
assertEquals(PackerOperationStatus.SUCCESS, result.status());
final Path assetRoot = projectRoot.resolve("assets/scenes/overworld");
assertTrue(Files.isRegularFile(assetRoot.resolve("asset.json")));
assertTrue(Files.isRegularFile(assetRoot.resolve("scene-bank.studio.json")));
final var supportFile = MAPPER.readTree(assetRoot.resolve("scene-bank.studio.json").toFile());
assertEquals(1, supportFile.path("layer_count").asInt());
assertEquals("layer-1.tmx", supportFile.path("layers").get(0).path("tilemap").asText());
}
@Test @Test
void returnsCreatedAssetThroughRuntimeBackedDetailsWithoutRescanMismatch() throws Exception { void returnsCreatedAssetThroughRuntimeBackedDetailsWithoutRescanMismatch() throws Exception {
final Path projectRoot = tempDir.resolve("created-details"); final Path projectRoot = tempDir.resolve("created-details");

View File

@ -67,6 +67,31 @@ final class PackerAssetDeclarationParserTest {
assertEquals(128, result.declaration().outputPipelineMetadata().get("samples").path("1").path("length").asInt()); assertEquals(128, result.declaration().outputPipelineMetadata().get("samples").path("1").path("length").asInt());
} }
@Test
void parsesSceneBankDeclaration() throws Exception {
final Path manifest = tempDir.resolve("scene-asset.json");
Files.writeString(manifest, """
{
"schema_version": 1,
"asset_uuid": "uuid-scene",
"name": "overworld_scene",
"type": "scene_bank",
"output": {
"format": "SCENE/tiled_v1",
"codec": "NONE"
},
"preload": { "enabled": false }
}
""");
final var result = parser.parse(manifest);
assertTrue(result.valid());
assertEquals(AssetFamilyCatalog.SCENE_BANK, result.declaration().assetFamily());
assertEquals("SCENE/tiled_v1", result.declaration().outputFormat().displayName());
assertEquals(OutputCodecCatalog.NONE, result.declaration().outputCodec());
}
@Test @Test
void rejectsNonObjectPipelineMetadata() throws Exception { void rejectsNonObjectPipelineMetadata() throws Exception {
final Path manifest = tempDir.resolve("asset.json"); final Path manifest = tempDir.resolve("asset.json");

View File

@ -190,10 +190,19 @@ public enum I18n {
ASSETS_LABEL_BUILD_PARTICIPATION("assets.label.buildParticipation"), ASSETS_LABEL_BUILD_PARTICIPATION("assets.label.buildParticipation"),
ASSETS_LABEL_ASSET_ID("assets.label.assetId"), ASSETS_LABEL_ASSET_ID("assets.label.assetId"),
ASSETS_LABEL_TYPE("assets.label.type"), ASSETS_LABEL_TYPE("assets.label.type"),
ASSETS_LABEL_STUDIO_ROLE("assets.label.studioRole"),
ASSETS_LABEL_SCENE_LAYERS("assets.label.sceneLayers"),
ASSETS_LABEL_TILEMAPS("assets.label.tilemaps"),
ASSETS_LABEL_SUPPORT_FILE("assets.label.supportFile"),
ASSETS_TYPE_GLYPH_BANK("assets.type.glyphBank"), ASSETS_TYPE_GLYPH_BANK("assets.type.glyphBank"),
ASSETS_TYPE_SCENE_BANK("assets.type.sceneBank"),
ASSETS_TYPE_PALETTE_BANK("assets.type.paletteBank"), ASSETS_TYPE_PALETTE_BANK("assets.type.paletteBank"),
ASSETS_TYPE_SOUND_BANK("assets.type.soundBank"), ASSETS_TYPE_SOUND_BANK("assets.type.soundBank"),
ASSETS_TYPE_UNKNOWN("assets.type.unknown"), ASSETS_TYPE_UNKNOWN("assets.type.unknown"),
ASSETS_SPECIALIZATION_NONE("assets.specialization.none"),
ASSETS_SPECIALIZATION_TILESET("assets.specialization.tileset"),
ASSETS_SPECIALIZATION_SPRITES("assets.specialization.sprites"),
ASSETS_SPECIALIZATION_UI("assets.specialization.ui"),
ASSETS_LABEL_LOCATION("assets.label.location"), ASSETS_LABEL_LOCATION("assets.label.location"),
ASSETS_LABEL_BANK("assets.label.bank"), ASSETS_LABEL_BANK("assets.label.bank"),
ASSETS_LABEL_TARGET_LOCATION("assets.label.targetLocation"), ASSETS_LABEL_TARGET_LOCATION("assets.label.targetLocation"),
@ -254,10 +263,12 @@ public enum I18n {
ASSETS_ADD_WIZARD_LABEL_NAME("assets.addWizard.label.name"), ASSETS_ADD_WIZARD_LABEL_NAME("assets.addWizard.label.name"),
ASSETS_ADD_WIZARD_LABEL_ROOT("assets.addWizard.label.root"), ASSETS_ADD_WIZARD_LABEL_ROOT("assets.addWizard.label.root"),
ASSETS_ADD_WIZARD_LABEL_TYPE("assets.addWizard.label.type"), ASSETS_ADD_WIZARD_LABEL_TYPE("assets.addWizard.label.type"),
ASSETS_ADD_WIZARD_LABEL_SPECIALIZATION("assets.addWizard.label.specialization"),
ASSETS_ADD_WIZARD_LABEL_FORMAT("assets.addWizard.label.format"), ASSETS_ADD_WIZARD_LABEL_FORMAT("assets.addWizard.label.format"),
ASSETS_ADD_WIZARD_LABEL_CODEC("assets.addWizard.label.codec"), ASSETS_ADD_WIZARD_LABEL_CODEC("assets.addWizard.label.codec"),
ASSETS_ADD_WIZARD_LABEL_PRELOAD("assets.addWizard.label.preload"), ASSETS_ADD_WIZARD_LABEL_PRELOAD("assets.addWizard.label.preload"),
ASSETS_ADD_WIZARD_PROMPT_TYPE("assets.addWizard.prompt.type"), ASSETS_ADD_WIZARD_PROMPT_TYPE("assets.addWizard.prompt.type"),
ASSETS_ADD_WIZARD_PROMPT_SPECIALIZATION("assets.addWizard.prompt.specialization"),
ASSETS_ADD_WIZARD_PROMPT_FORMAT("assets.addWizard.prompt.format"), ASSETS_ADD_WIZARD_PROMPT_FORMAT("assets.addWizard.prompt.format"),
ASSETS_ADD_WIZARD_PROMPT_CODEC("assets.addWizard.prompt.codec"), ASSETS_ADD_WIZARD_PROMPT_CODEC("assets.addWizard.prompt.codec"),
ASSETS_ADD_WIZARD_ASSETS_ROOT_HINT("assets.addWizard.assetsRootHint"), ASSETS_ADD_WIZARD_ASSETS_ROOT_HINT("assets.addWizard.assetsRootHint"),

View File

@ -23,6 +23,8 @@ import p.studio.workspaces.assets.details.bank.AssetDetailsBankCompositionContro
import p.studio.workspaces.assets.details.contract.AssetDetailsContractControl; import p.studio.workspaces.assets.details.contract.AssetDetailsContractControl;
import p.studio.workspaces.assets.details.palette.AssetDetailsPaletteOverhaulingControl; import p.studio.workspaces.assets.details.palette.AssetDetailsPaletteOverhaulingControl;
import p.studio.workspaces.assets.details.summary.AssetDetailsSummaryControl; import p.studio.workspaces.assets.details.summary.AssetDetailsSummaryControl;
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
import p.studio.workspaces.assets.metadata.AssetStudioMetadataSnapshot;
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetAction; import p.studio.workspaces.assets.messages.AssetWorkspaceAssetAction;
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionDetails; import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionDetails;
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile; import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
@ -53,6 +55,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
private final AssetDetailsContractControl contractControl; private final AssetDetailsContractControl contractControl;
private final AssetDetailsBankCompositionControl bankCompositionControl; private final AssetDetailsBankCompositionControl bankCompositionControl;
private final AssetDetailsPaletteOverhaulingControl paletteOverhaulingControl; private final AssetDetailsPaletteOverhaulingControl paletteOverhaulingControl;
private final AssetStudioMetadataService studioMetadataService = new AssetStudioMetadataService();
private final VBox actionsContent = new VBox(10); private final VBox actionsContent = new VBox(10);
private final ScrollPane actionsScroll = new ScrollPane(); private final ScrollPane actionsScroll = new ScrollPane();
private final VBox actionsSection; private final VBox actionsSection;
@ -534,6 +537,10 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
PackerAssetDetailsDTO details, PackerAssetDetailsDTO details,
java.util.List<PackerDiagnosticDTO> diagnostics, java.util.List<PackerDiagnosticDTO> diagnostics,
java.util.List<PackerAssetActionAvailabilityDTO> actions) { java.util.List<PackerAssetActionAvailabilityDTO> actions) {
final var baseSummary = AssetListPackerMappings.mapSummary(details.summary());
final AssetStudioMetadataSnapshot studioMetadata = studioMetadataService.read(
baseSummary.assetRoot(),
baseSummary.assetFamily());
final java.util.List<PackerDiagnosticDTO> mergedDiagnostics = new java.util.ArrayList<>(details.diagnostics()); final java.util.List<PackerDiagnosticDTO> mergedDiagnostics = new java.util.ArrayList<>(details.diagnostics());
for (PackerDiagnosticDTO diagnostic : diagnostics) { for (PackerDiagnosticDTO diagnostic : diagnostics) {
if (!mergedDiagnostics.contains(diagnostic)) { if (!mergedDiagnostics.contains(diagnostic)) {
@ -541,7 +548,17 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
} }
} }
return new AssetWorkspaceAssetDetails( return new AssetWorkspaceAssetDetails(
AssetListPackerMappings.mapSummary(details.summary()), new p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary(
baseSummary.assetReference(),
baseSummary.assetName(),
baseSummary.state(),
baseSummary.buildParticipation(),
baseSummary.assetId(),
baseSummary.assetFamily(),
studioMetadata.glyphSpecialization(),
baseSummary.assetRoot(),
baseSummary.preload(),
baseSummary.hasDiagnostics()),
actions.stream() actions.stream()
.map(action -> new AssetWorkspaceAssetAction( .map(action -> new AssetWorkspaceAssetAction(
action.action(), action.action(),
@ -557,6 +574,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
details.outputPipeline(), details.outputPipeline(),
mapBankComposition(details.bankComposition()), mapBankComposition(details.bankComposition()),
details.pipelinePalettes(), details.pipelinePalettes(),
studioMetadata.sceneBankMetadata(),
mergedDiagnostics); mergedDiagnostics);
} }

View File

@ -16,6 +16,7 @@ import p.studio.controls.forms.StudioFormSection;
import p.studio.controls.forms.StudioSection; import p.studio.controls.forms.StudioSection;
import p.studio.projects.ProjectReference; import p.studio.projects.ProjectReference;
import p.studio.utilities.i18n.I18n; import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetState; import p.studio.workspaces.assets.messages.AssetWorkspaceAssetState;
import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation; import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation;
@ -114,6 +115,7 @@ public final class AssetDetailsUiSupport {
public static String typeLabel(AssetFamilyCatalog assetFamily) { public static String typeLabel(AssetFamilyCatalog assetFamily) {
return switch (assetFamily) { return switch (assetFamily) {
case GLYPH_BANK -> Container.i18n().text(I18n.ASSETS_TYPE_GLYPH_BANK); case GLYPH_BANK -> Container.i18n().text(I18n.ASSETS_TYPE_GLYPH_BANK);
case SCENE_BANK -> Container.i18n().text(I18n.ASSETS_TYPE_SCENE_BANK);
case SOUND_BANK -> Container.i18n().text(I18n.ASSETS_TYPE_SOUND_BANK); case SOUND_BANK -> Container.i18n().text(I18n.ASSETS_TYPE_SOUND_BANK);
case UNKNOWN -> Container.i18n().text(I18n.ASSETS_TYPE_UNKNOWN); case UNKNOWN -> Container.i18n().text(I18n.ASSETS_TYPE_UNKNOWN);
}; };
@ -122,11 +124,21 @@ public final class AssetDetailsUiSupport {
public static String typeChipTone(AssetFamilyCatalog assetFamily) { public static String typeChipTone(AssetFamilyCatalog assetFamily) {
return switch (assetFamily) { return switch (assetFamily) {
case GLYPH_BANK -> "assets-details-chip-image"; case GLYPH_BANK -> "assets-details-chip-image";
case SCENE_BANK -> "assets-details-chip-generic";
case SOUND_BANK -> "assets-details-chip-audio"; case SOUND_BANK -> "assets-details-chip-audio";
case UNKNOWN -> "assets-details-chip-generic"; case UNKNOWN -> "assets-details-chip-generic";
}; };
} }
public static String specializationLabel(AssetStudioGlyphSpecialization specialization) {
return switch (specialization) {
case NONE -> Container.i18n().text(I18n.ASSETS_SPECIALIZATION_NONE);
case TILESET -> Container.i18n().text(I18n.ASSETS_SPECIALIZATION_TILESET);
case SPRITES -> Container.i18n().text(I18n.ASSETS_SPECIALIZATION_SPRITES);
case UI -> Container.i18n().text(I18n.ASSETS_SPECIALIZATION_UI);
};
}
public static String actionLabel(AssetAction action) { public static String actionLabel(AssetAction action) {
return switch (action) { return switch (action) {
case REGISTER -> Container.i18n().text(I18n.ASSETS_ACTION_REGISTER); case REGISTER -> Container.i18n().text(I18n.ASSETS_ACTION_REGISTER);

View File

@ -1,6 +1,7 @@
package p.studio.workspaces.assets.details; package p.studio.workspaces.assets.details;
import p.packer.dtos.PackerAssetSummaryDTO; import p.packer.dtos.PackerAssetSummaryDTO;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetState; import p.studio.workspaces.assets.messages.AssetWorkspaceAssetState;
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary; import p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary;
import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation; import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation;
@ -25,6 +26,7 @@ public final class AssetListPackerMappings {
buildParticipation, buildParticipation,
summary.identity().assetId(), summary.identity().assetId(),
summary.assetFamily(), summary.assetFamily(),
AssetStudioGlyphSpecialization.NONE,
summary.identity().assetRoot(), summary.identity().assetRoot(),
summary.preloadEnabled(), summary.preloadEnabled(),
summary.hasDiagnostics()); summary.hasDiagnostics());

View File

@ -21,6 +21,12 @@ public final class AssetBankCapacityService {
final Map<String, Object> safePipeline = Map.copyOf(Objects.requireNonNull(outputPipeline, "outputPipeline")); final Map<String, Object> safePipeline = Map.copyOf(Objects.requireNonNull(outputPipeline, "outputPipeline"));
return switch (safeFamily) { return switch (safeFamily) {
case GLYPH_BANK -> evaluateGlyphBank(artifactCount, safeMetadata); case GLYPH_BANK -> evaluateGlyphBank(artifactCount, safeMetadata);
case SCENE_BANK -> new AssetDetailsBankCompositionCapacityState(
0.0d,
StudioAssetCapacitySeverity.GREEN,
false,
artifactCount + " support files",
"");
case SOUND_BANK -> evaluateSoundBank(resolveSoundBankUsedBytes(safePipeline, usedBytes)); case SOUND_BANK -> evaluateSoundBank(resolveSoundBankUsedBytes(safePipeline, usedBytes));
case UNKNOWN -> new AssetDetailsBankCompositionCapacityState( case UNKNOWN -> new AssetDetailsBankCompositionCapacityState(
0.0d, 0.0d,

View File

@ -8,6 +8,9 @@ import p.studio.lsp.events.StudioWorkspaceEventBus;
import p.studio.projects.ProjectReference; import p.studio.projects.ProjectReference;
import p.studio.utilities.i18n.I18n; import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.assets.details.AssetDetailsUiSupport; import p.studio.workspaces.assets.details.AssetDetailsUiSupport;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary; import p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary;
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState; import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent; import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
@ -53,14 +56,40 @@ public final class AssetDetailsSummaryControl extends VBox implements StudioCont
} }
final VBox content = new VBox(8); final VBox content = new VBox(8);
final AssetStudioSceneBankMetadata sceneBankMetadata = viewState.selectedAssetDetails().sceneBankMetadata();
content.getChildren().setAll( content.getChildren().setAll(
AssetDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_ASSET_ID), summary.assetId() == null ? "" : String.valueOf(summary.assetId())), AssetDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_ASSET_ID), summary.assetId() == null ? "" : String.valueOf(summary.assetId())),
AssetDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), AssetDetailsUiSupport.projectRelativePath(projectReference, summary.assetRoot()))); AssetDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), AssetDetailsUiSupport.projectRelativePath(projectReference, summary.assetRoot())));
final VBox typeBox = new VBox(6);
typeBox.getChildren().add(AssetDetailsUiSupport.createChip(
AssetDetailsUiSupport.typeChipTone(summary.assetFamily()),
AssetDetailsUiSupport.typeLabel(summary.assetFamily())));
if (summary.glyphSpecialization() != AssetStudioGlyphSpecialization.NONE) {
typeBox.getChildren().add(AssetDetailsUiSupport.createChip(
"assets-details-chip-generic",
AssetDetailsUiSupport.specializationLabel(summary.glyphSpecialization())));
}
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow( content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
Container.i18n().text(I18n.ASSETS_LABEL_TYPE), Container.i18n().text(I18n.ASSETS_LABEL_TYPE),
AssetDetailsUiSupport.createChip( typeBox));
AssetDetailsUiSupport.typeChipTone(summary.assetFamily()), if (summary.glyphSpecialization() != AssetStudioGlyphSpecialization.NONE) {
AssetDetailsUiSupport.typeLabel(summary.assetFamily())))); content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
Container.i18n().text(I18n.ASSETS_LABEL_STUDIO_ROLE),
AssetDetailsUiSupport.specializationLabel(summary.glyphSpecialization())));
}
if (sceneBankMetadata != null) {
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
Container.i18n().text(I18n.ASSETS_LABEL_SCENE_LAYERS),
String.valueOf(sceneBankMetadata.layerCount())));
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
Container.i18n().text(I18n.ASSETS_LABEL_TILEMAPS),
sceneBankMetadata.layerBindings().stream()
.map(AssetStudioSceneLayerBinding::tilemap)
.collect(java.util.stream.Collectors.joining(", "))));
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
Container.i18n().text(I18n.ASSETS_LABEL_SUPPORT_FILE),
AssetDetailsUiSupport.projectRelativePath(projectReference, sceneBankMetadata.supportFile())));
}
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow( content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
Container.i18n().text(I18n.ASSETS_LABEL_REGISTRATION), Container.i18n().text(I18n.ASSETS_LABEL_REGISTRATION),
AssetDetailsUiSupport.createChip( AssetDetailsUiSupport.createChip(

View File

@ -113,6 +113,7 @@ public final class AssetListItemControl extends VBox {
private String assetRowToneClass(AssetFamilyCatalog assetFamily) { private String assetRowToneClass(AssetFamilyCatalog assetFamily) {
return switch (assetFamily) { return switch (assetFamily) {
case GLYPH_BANK -> "assets-workspace-asset-row-tone-image"; case GLYPH_BANK -> "assets-workspace-asset-row-tone-image";
case SCENE_BANK -> "assets-workspace-asset-row-tone-generic";
case SOUND_BANK -> "assets-workspace-asset-row-tone-audio"; case SOUND_BANK -> "assets-workspace-asset-row-tone-audio";
default -> "assets-workspace-asset-row-tone-generic"; default -> "assets-workspace-asset-row-tone-generic";
}; };
@ -121,6 +122,7 @@ public final class AssetListItemControl extends VBox {
private String assetNameToneClass(AssetFamilyCatalog assetFamily) { private String assetNameToneClass(AssetFamilyCatalog assetFamily) {
return switch (assetFamily) { return switch (assetFamily) {
case GLYPH_BANK -> "assets-workspace-asset-name-tone-image"; case GLYPH_BANK -> "assets-workspace-asset-name-tone-image";
case SCENE_BANK -> "assets-workspace-asset-name-tone-generic";
case SOUND_BANK -> "assets-workspace-asset-name-tone-audio"; case SOUND_BANK -> "assets-workspace-asset-name-tone-audio";
default -> "assets-workspace-asset-name-tone-generic"; default -> "assets-workspace-asset-name-tone-generic";
}; };

View File

@ -4,6 +4,7 @@ import p.packer.dtos.PackerCodecConfigurationFieldDTO;
import p.packer.dtos.PackerDiagnosticDTO; import p.packer.dtos.PackerDiagnosticDTO;
import p.packer.messages.assets.OutputCodecCatalog; import p.packer.messages.assets.OutputCodecCatalog;
import p.packer.messages.assets.OutputFormatCatalog; import p.packer.messages.assets.OutputFormatCatalog;
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -20,6 +21,7 @@ public record AssetWorkspaceAssetDetails(
Map<String, Object> outputPipeline, Map<String, Object> outputPipeline,
AssetWorkspaceBankCompositionDetails bankComposition, AssetWorkspaceBankCompositionDetails bankComposition,
List<Map<String, Object>> pipelinePalettes, List<Map<String, Object>> pipelinePalettes,
AssetStudioSceneBankMetadata sceneBankMetadata,
List<PackerDiagnosticDTO> diagnostics) { List<PackerDiagnosticDTO> diagnostics) {
public AssetWorkspaceAssetDetails { public AssetWorkspaceAssetDetails {

View File

@ -2,6 +2,7 @@ package p.studio.workspaces.assets.messages;
import p.packer.messages.AssetReference; import p.packer.messages.AssetReference;
import p.packer.messages.assets.AssetFamilyCatalog; import p.packer.messages.assets.AssetFamilyCatalog;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Objects; import java.util.Objects;
@ -13,6 +14,7 @@ public record AssetWorkspaceAssetSummary(
AssetWorkspaceBuildParticipation buildParticipation, AssetWorkspaceBuildParticipation buildParticipation,
Integer assetId, Integer assetId,
AssetFamilyCatalog assetFamily, AssetFamilyCatalog assetFamily,
AssetStudioGlyphSpecialization glyphSpecialization,
Path assetRoot, Path assetRoot,
boolean preload, boolean preload,
boolean hasDiagnostics) { boolean hasDiagnostics) {
@ -23,6 +25,7 @@ public record AssetWorkspaceAssetSummary(
Objects.requireNonNull(state, "state"); Objects.requireNonNull(state, "state");
Objects.requireNonNull(buildParticipation, "buildParticipation"); Objects.requireNonNull(buildParticipation, "buildParticipation");
assetFamily = Objects.requireNonNullElse(assetFamily, AssetFamilyCatalog.UNKNOWN); assetFamily = Objects.requireNonNullElse(assetFamily, AssetFamilyCatalog.UNKNOWN);
glyphSpecialization = Objects.requireNonNullElse(glyphSpecialization, AssetStudioGlyphSpecialization.NONE);
assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize(); assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
if (assetName.isBlank()) { if (assetName.isBlank()) {
throw new IllegalArgumentException("assetName must not be blank"); throw new IllegalArgumentException("assetName must not be blank");

View File

@ -0,0 +1,33 @@
package p.studio.workspaces.assets.metadata;
import java.util.Locale;
public enum AssetStudioGlyphSpecialization {
NONE("none"),
TILESET("tileset"),
SPRITES("sprites"),
UI("ui");
private final String manifestValue;
AssetStudioGlyphSpecialization(String manifestValue) {
this.manifestValue = manifestValue;
}
public String manifestValue() {
return manifestValue;
}
public static AssetStudioGlyphSpecialization fromManifestValue(String value) {
if (value == null) {
return NONE;
}
final String normalized = value.trim().toLowerCase(Locale.ROOT);
for (AssetStudioGlyphSpecialization candidate : values()) {
if (candidate.manifestValue.equals(normalized)) {
return candidate;
}
}
return NONE;
}
}

View File

@ -0,0 +1,104 @@
package p.studio.workspaces.assets.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import p.packer.messages.assets.AssetFamilyCatalog;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
public final class AssetStudioMetadataService {
public static final String STUDIO_ASSET_METADATA_FILE = "studio.asset.json";
public static final String SCENE_BANK_SUPPORT_FILE = "scene-bank.studio.json";
private final ObjectMapper mapper = new ObjectMapper();
public AssetStudioMetadataSnapshot read(Path assetRoot, AssetFamilyCatalog assetFamily) {
final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
return new AssetStudioMetadataSnapshot(
readGlyphSpecialization(normalizedAssetRoot, assetFamily),
readSceneBankMetadata(normalizedAssetRoot, assetFamily));
}
public void writeGlyphSpecialization(Path assetRoot, AssetStudioGlyphSpecialization specialization) throws IOException {
final Path metadataPath = Objects.requireNonNull(assetRoot, "assetRoot")
.toAbsolutePath()
.normalize()
.resolve(STUDIO_ASSET_METADATA_FILE);
final AssetStudioGlyphSpecialization normalized = Objects.requireNonNullElse(
specialization,
AssetStudioGlyphSpecialization.NONE);
if (normalized == AssetStudioGlyphSpecialization.NONE) {
Files.deleteIfExists(metadataPath);
return;
}
final var root = mapper.createObjectNode();
root.put("schema_version", 1);
root.put("glyph_bank_specialization", normalized.manifestValue());
mapper.writerWithDefaultPrettyPrinter().writeValue(metadataPath.toFile(), root);
}
private AssetStudioGlyphSpecialization readGlyphSpecialization(Path assetRoot, AssetFamilyCatalog assetFamily) {
if (assetFamily != AssetFamilyCatalog.GLYPH_BANK) {
return AssetStudioGlyphSpecialization.NONE;
}
final Path metadataPath = assetRoot.resolve(STUDIO_ASSET_METADATA_FILE);
if (!Files.isRegularFile(metadataPath)) {
return AssetStudioGlyphSpecialization.NONE;
}
try {
final JsonNode root = mapper.readTree(metadataPath.toFile());
if (root == null || !root.isObject()) {
return AssetStudioGlyphSpecialization.NONE;
}
return AssetStudioGlyphSpecialization.fromManifestValue(root.path("glyph_bank_specialization").asText(null));
} catch (IOException ignored) {
return AssetStudioGlyphSpecialization.NONE;
}
}
private AssetStudioSceneBankMetadata readSceneBankMetadata(Path assetRoot, AssetFamilyCatalog assetFamily) {
if (assetFamily != AssetFamilyCatalog.SCENE_BANK) {
return null;
}
final Path supportFile = assetRoot.resolve(SCENE_BANK_SUPPORT_FILE);
if (!Files.isRegularFile(supportFile)) {
return null;
}
try {
final JsonNode root = mapper.readTree(supportFile.toFile());
if (root == null || !root.isObject()) {
return null;
}
final int layerCount = root.path("layer_count").asInt(0);
if (layerCount < 1 || layerCount > 4) {
return null;
}
if (!(root.path("layers") instanceof ArrayNode layersNode) || layersNode.size() != layerCount) {
return null;
}
final List<AssetStudioSceneLayerBinding> bindings = new ArrayList<>();
final Set<Integer> indexes = new HashSet<>();
for (JsonNode layerNode : layersNode) {
final int index = layerNode.path("index").asInt(0);
final String tilemap = layerNode.path("tilemap").asText("").trim();
if (index < 1 || index > layerCount || tilemap.isBlank() || !indexes.add(index)) {
return null;
}
bindings.add(new AssetStudioSceneLayerBinding(index, tilemap));
}
bindings.sort(Comparator.comparingInt(AssetStudioSceneLayerBinding::index));
return new AssetStudioSceneBankMetadata(layerCount, bindings, supportFile);
} catch (IOException ignored) {
return null;
}
}
}

View File

@ -0,0 +1,12 @@
package p.studio.workspaces.assets.metadata;
import java.util.Objects;
public record AssetStudioMetadataSnapshot(
AssetStudioGlyphSpecialization glyphSpecialization,
AssetStudioSceneBankMetadata sceneBankMetadata) {
public AssetStudioMetadataSnapshot {
glyphSpecialization = Objects.requireNonNullElse(glyphSpecialization, AssetStudioGlyphSpecialization.NONE);
}
}

View File

@ -0,0 +1,22 @@
package p.studio.workspaces.assets.metadata;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
public record AssetStudioSceneBankMetadata(
int layerCount,
List<AssetStudioSceneLayerBinding> layerBindings,
Path supportFile) {
public AssetStudioSceneBankMetadata {
layerBindings = List.copyOf(Objects.requireNonNull(layerBindings, "layerBindings"));
supportFile = Objects.requireNonNull(supportFile, "supportFile").toAbsolutePath().normalize();
if (layerCount < 1 || layerCount > 4) {
throw new IllegalArgumentException("layerCount must stay between 1 and 4");
}
if (layerBindings.size() != layerCount) {
throw new IllegalArgumentException("layerBindings must match layerCount");
}
}
}

View File

@ -0,0 +1,15 @@
package p.studio.workspaces.assets.metadata;
import java.util.Objects;
public record AssetStudioSceneLayerBinding(int index, String tilemap) {
public AssetStudioSceneLayerBinding {
tilemap = Objects.requireNonNull(tilemap, "tilemap").trim();
if (index <= 0) {
throw new IllegalArgumentException("index must be positive");
}
if (tilemap.isBlank()) {
throw new IllegalArgumentException("tilemap must not be blank");
}
}
}

View File

@ -24,6 +24,8 @@ import p.studio.Container;
import p.studio.projects.ProjectReference; import p.studio.projects.ProjectReference;
import p.studio.utilities.i18n.I18n; import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.assets.details.AssetDetailsUiSupport; import p.studio.workspaces.assets.details.AssetDetailsUiSupport;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -47,9 +49,11 @@ public final class AddAssetWizard {
private final TextField assetRootField = new TextField(); private final TextField assetRootField = new TextField();
private final TextField assetNameField = new TextField(); private final TextField assetNameField = new TextField();
private final ComboBox<AssetFamilyCatalog> assetFamilyCombo = new ComboBox<>(); private final ComboBox<AssetFamilyCatalog> assetFamilyCombo = new ComboBox<>();
private final ComboBox<AssetStudioGlyphSpecialization> glyphSpecializationCombo = new ComboBox<>();
private final ComboBox<OutputFormatCatalog> outputFormatCombo = new ComboBox<>(); private final ComboBox<OutputFormatCatalog> outputFormatCombo = new ComboBox<>();
private final ComboBox<OutputCodecCatalog> outputCodecCombo = new ComboBox<>(); private final ComboBox<OutputCodecCatalog> outputCodecCombo = new ComboBox<>();
private final CheckBox preloadCheckBox = new CheckBox(); private final CheckBox preloadCheckBox = new CheckBox();
private final AssetStudioMetadataService studioMetadataService = new AssetStudioMetadataService();
private int stepIndex; private int stepIndex;
private boolean creating; private boolean creating;
@ -65,6 +69,7 @@ public final class AddAssetWizard {
preloadCheckBox.setSelected(false); preloadCheckBox.setSelected(false);
configureAssetFamilyCombo(); configureAssetFamilyCombo();
configureGlyphSpecializationCombo();
configureOutputFormatCombo(); configureOutputFormatCombo();
configureOutputCodecCombo(); configureOutputCodecCombo();
renderStep(); renderStep();
@ -131,7 +136,34 @@ public final class AddAssetWizard {
}); });
assetFamilyCombo.valueProperty().addListener((ignored, oldValue, newValue) -> { assetFamilyCombo.valueProperty().addListener((ignored, oldValue, newValue) -> {
if (!Objects.equals(oldValue, newValue)) { if (!Objects.equals(oldValue, newValue)) {
if (newValue != AssetFamilyCatalog.GLYPH_BANK) {
glyphSpecializationCombo.getSelectionModel().select(AssetStudioGlyphSpecialization.NONE);
}
refreshOutputFormats(); refreshOutputFormats();
if (stepIndex == 1) {
renderStep();
}
}
});
}
private void configureGlyphSpecializationCombo() {
glyphSpecializationCombo.setItems(FXCollections.observableArrayList(AssetStudioGlyphSpecialization.values()));
glyphSpecializationCombo.setMaxWidth(Double.MAX_VALUE);
glyphSpecializationCombo.setPromptText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_PROMPT_SPECIALIZATION));
glyphSpecializationCombo.getSelectionModel().select(AssetStudioGlyphSpecialization.NONE);
glyphSpecializationCombo.setCellFactory(ignored -> new javafx.scene.control.ListCell<>() {
@Override
protected void updateItem(AssetStudioGlyphSpecialization item, boolean empty) {
super.updateItem(item, empty);
setText(empty || item == null ? null : AssetDetailsUiSupport.specializationLabel(item));
}
});
glyphSpecializationCombo.setButtonCell(new javafx.scene.control.ListCell<>() {
@Override
protected void updateItem(AssetStudioGlyphSpecialization item, boolean empty) {
super.updateItem(item, empty);
setText(empty || item == null ? null : AssetDetailsUiSupport.specializationLabel(item));
} }
}); });
} }
@ -224,17 +256,23 @@ public final class AddAssetWizard {
final Label nameLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_NAME)); final Label nameLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_NAME));
final Label typeLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_TYPE)); final Label typeLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_TYPE));
final Label specializationLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_SPECIALIZATION));
final Label formatLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_FORMAT)); final Label formatLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_FORMAT));
final Label codecLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_CODEC)); final Label codecLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_CODEC));
final Label preloadLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_PRELOAD)); final Label preloadLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_PRELOAD));
final Label noteLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_NOTE)); final Label noteLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_NOTE));
noteLabel.setWrapText(true); noteLabel.setWrapText(true);
noteLabel.getStyleClass().add("studio-launcher-subtitle"); noteLabel.getStyleClass().add("studio-launcher-subtitle");
final VBox specializationBox = new VBox(6, specializationLabel, glyphSpecializationCombo);
final boolean specializationVisible = selectedFamily() == AssetFamilyCatalog.GLYPH_BANK;
specializationBox.setVisible(specializationVisible);
specializationBox.setManaged(specializationVisible);
preloadCheckBox.setText(""); preloadCheckBox.setText("");
stepBody.getChildren().setAll( stepBody.getChildren().setAll(
new VBox(6, nameLabel, assetNameField), new VBox(6, nameLabel, assetNameField),
new VBox(6, typeLabel, assetFamilyCombo), new VBox(6, typeLabel, assetFamilyCombo),
specializationBox,
new VBox(6, formatLabel, outputFormatCombo), new VBox(6, formatLabel, outputFormatCombo),
new VBox(6, codecLabel, outputCodecCombo), new VBox(6, codecLabel, outputCodecCombo),
new VBox(6, preloadLabel, preloadCheckBox), new VBox(6, preloadLabel, preloadCheckBox),
@ -360,6 +398,15 @@ public final class AddAssetWizard {
private void applyCreateResult(CreateAssetResult createResult) { private void applyCreateResult(CreateAssetResult createResult) {
creating = false; creating = false;
if (createResult.status() == PackerOperationStatus.SUCCESS && createResult.assetReference() != null) { if (createResult.status() == PackerOperationStatus.SUCCESS && createResult.assetReference() != null) {
try {
persistStudioMetadata(createResult.assetRoot());
} catch (IOException exception) {
feedbackLabel.setText(exception.getMessage() == null || exception.getMessage().isBlank()
? "Unable to persist Studio metadata."
: exception.getMessage());
renderStep();
return;
}
result.set(createResult.assetReference()); result.set(createResult.assetReference());
stage.close(); stage.close();
return; return;
@ -409,6 +456,17 @@ public final class AddAssetWizard {
return outputCodecCombo.getValue(); return outputCodecCombo.getValue();
} }
private AssetStudioGlyphSpecialization selectedGlyphSpecialization() {
return glyphSpecializationCombo.getValue();
}
private void persistStudioMetadata(Path assetRoot) throws IOException {
if (selectedFamily() != AssetFamilyCatalog.GLYPH_BANK) {
return;
}
studioMetadataService.writeGlyphSpecialization(assetRoot, selectedGlyphSpecialization());
}
private String normalizedRelativeRoot(String candidate) { private String normalizedRelativeRoot(String candidate) {
final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/'); final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/');
if (raw.isBlank()) { if (raw.isBlank()) {

View File

@ -181,10 +181,19 @@ assets.label.registration=Registration
assets.label.buildParticipation=Build Participation assets.label.buildParticipation=Build Participation
assets.label.assetId=Asset ID assets.label.assetId=Asset ID
assets.label.type=Type assets.label.type=Type
assets.label.studioRole=Studio Role
assets.label.sceneLayers=Scene Layers
assets.label.tilemaps=Tilemaps
assets.label.supportFile=Support File
assets.type.glyphBank=Glyph Bank assets.type.glyphBank=Glyph Bank
assets.type.sceneBank=Scene Bank
assets.type.paletteBank=Palette Bank assets.type.paletteBank=Palette Bank
assets.type.soundBank=Sound Bank assets.type.soundBank=Sound Bank
assets.type.unknown=Unknown assets.type.unknown=Unknown
assets.specialization.none=None
assets.specialization.tileset=Tileset
assets.specialization.sprites=Sprites
assets.specialization.ui=UI
assets.label.location=Location assets.label.location=Location
assets.label.bank=Bank assets.label.bank=Bank
assets.label.targetLocation=Target Location assets.label.targetLocation=Target Location
@ -245,10 +254,12 @@ assets.addWizard.step.summary.description=Confirm the registered asset you are a
assets.addWizard.label.name=Asset Name assets.addWizard.label.name=Asset Name
assets.addWizard.label.root=Asset Root assets.addWizard.label.root=Asset Root
assets.addWizard.label.type=Asset Type assets.addWizard.label.type=Asset Type
assets.addWizard.label.specialization=Studio Specialization
assets.addWizard.label.format=Output Format assets.addWizard.label.format=Output Format
assets.addWizard.label.codec=Output Codec assets.addWizard.label.codec=Output Codec
assets.addWizard.label.preload=Preload on startup assets.addWizard.label.preload=Preload on startup
assets.addWizard.prompt.type=Choose asset type assets.addWizard.prompt.type=Choose asset type
assets.addWizard.prompt.specialization=Choose Studio specialization
assets.addWizard.prompt.format=Choose output format assets.addWizard.prompt.format=Choose output format
assets.addWizard.prompt.codec=Choose output codec assets.addWizard.prompt.codec=Choose output codec
assets.addWizard.assetsRootHint=Assets root: {0} assets.addWizard.assetsRootHint=Assets root: {0}

View File

@ -7,6 +7,7 @@ import p.packer.messages.assets.AssetFamilyCatalog;
import p.packer.messages.assets.OutputCodecCatalog; import p.packer.messages.assets.OutputCodecCatalog;
import p.packer.messages.assets.OutputFormatCatalog; import p.packer.messages.assets.OutputFormatCatalog;
import p.packer.messages.assets.PackerCodecConfigurationFieldType; import p.packer.messages.assets.PackerCodecConfigurationFieldType;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
import p.studio.workspaces.assets.messages.*; import p.studio.workspaces.assets.messages.*;
import java.nio.file.Path; import java.nio.file.Path;
@ -114,6 +115,7 @@ final class AssetDetailsBankCompositionCoordinatorTest {
List.of(), List.of(),
0L), 0L),
List.of(), List.of(),
null,
List.of()); List.of());
} }
@ -136,6 +138,7 @@ final class AssetDetailsBankCompositionCoordinatorTest {
List.of(), List.of(),
0L), 0L),
List.of(), List.of(),
null,
List.of()); List.of());
} }
@ -159,6 +162,7 @@ final class AssetDetailsBankCompositionCoordinatorTest {
AssetWorkspaceBuildParticipation.INCLUDED, AssetWorkspaceBuildParticipation.INCLUDED,
1, 1,
family, family,
AssetStudioGlyphSpecialization.NONE,
Path.of("/tmp/bank"), Path.of("/tmp/bank"),
false, false,
false); false);

View File

@ -5,6 +5,7 @@ import p.packer.messages.AssetReference;
import p.packer.messages.assets.AssetFamilyCatalog; import p.packer.messages.assets.AssetFamilyCatalog;
import p.packer.messages.assets.OutputCodecCatalog; import p.packer.messages.assets.OutputCodecCatalog;
import p.packer.messages.assets.OutputFormatCatalog; import p.packer.messages.assets.OutputFormatCatalog;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
import p.studio.workspaces.assets.messages.*; import p.studio.workspaces.assets.messages.*;
import java.nio.file.Path; import java.nio.file.Path;
@ -81,6 +82,7 @@ final class AssetDetailsPaletteOverhaulingCoordinatorTest {
AssetWorkspaceBuildParticipation.INCLUDED, AssetWorkspaceBuildParticipation.INCLUDED,
1, 1,
AssetFamilyCatalog.GLYPH_BANK, AssetFamilyCatalog.GLYPH_BANK,
AssetStudioGlyphSpecialization.NONE,
Path.of("/tmp/bank"), Path.of("/tmp/bank"),
false, false,
false), false),
@ -93,6 +95,7 @@ final class AssetDetailsPaletteOverhaulingCoordinatorTest {
Map.of(), Map.of(),
new AssetWorkspaceBankCompositionDetails(availableFiles, selectedFiles, 0L), new AssetWorkspaceBankCompositionDetails(availableFiles, selectedFiles, 0L),
selectedFiles.stream().map(file -> (Map<String, Object>) file.metadata().get("palette")).toList(), selectedFiles.stream().map(file -> (Map<String, Object>) file.metadata().get("palette")).toList(),
null,
List.of()); List.of());
} }

View File

@ -0,0 +1,76 @@
package p.studio.workspaces.assets.metadata;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.packer.messages.assets.AssetFamilyCatalog;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
final class AssetStudioMetadataServiceTest {
@TempDir
Path tempDir;
private final AssetStudioMetadataService service = new AssetStudioMetadataService();
@Test
void readsGlyphSpecializationFromStudioMetadataFile() throws Exception {
final Path assetRoot = tempDir.resolve("tileset");
Files.createDirectories(assetRoot);
service.writeGlyphSpecialization(assetRoot, AssetStudioGlyphSpecialization.TILESET);
final AssetStudioMetadataSnapshot snapshot = service.read(assetRoot, AssetFamilyCatalog.GLYPH_BANK);
assertEquals(AssetStudioGlyphSpecialization.TILESET, snapshot.glyphSpecialization());
assertNull(snapshot.sceneBankMetadata());
}
@Test
void readsSceneBankSupportMetadataWhenContractIsValid() throws Exception {
final Path assetRoot = tempDir.resolve("scene");
Files.createDirectories(assetRoot);
Files.writeString(assetRoot.resolve(AssetStudioMetadataService.SCENE_BANK_SUPPORT_FILE), """
{
"schema_version": 1,
"layer_count": 2,
"layers": [
{ "index": 1, "tilemap": "ground.tmx" },
{ "index": 2, "tilemap": "collision.tmx" }
]
}
""");
final AssetStudioMetadataSnapshot snapshot = service.read(assetRoot, AssetFamilyCatalog.SCENE_BANK);
assertNotNull(snapshot.sceneBankMetadata());
assertEquals(2, snapshot.sceneBankMetadata().layerCount());
assertEquals(
java.util.List.of("ground.tmx", "collision.tmx"),
snapshot.sceneBankMetadata().layerBindings().stream().map(AssetStudioSceneLayerBinding::tilemap).toList());
}
@Test
void rejectsSceneBankSupportMetadataBeyondWaveOneLayerLimit() throws Exception {
final Path assetRoot = tempDir.resolve("scene-invalid");
Files.createDirectories(assetRoot);
Files.writeString(assetRoot.resolve(AssetStudioMetadataService.SCENE_BANK_SUPPORT_FILE), """
{
"schema_version": 1,
"layer_count": 5,
"layers": [
{ "index": 1, "tilemap": "a.tmx" },
{ "index": 2, "tilemap": "b.tmx" },
{ "index": 3, "tilemap": "c.tmx" },
{ "index": 4, "tilemap": "d.tmx" },
{ "index": 5, "tilemap": "e.tmx" }
]
}
""");
final AssetStudioMetadataSnapshot snapshot = service.read(assetRoot, AssetFamilyCatalog.SCENE_BANK);
assertNull(snapshot.sceneBankMetadata());
}
}