From bbd588eed460346e0c9c8730def0be83e4aee715 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sat, 18 Apr 2026 17:46:04 +0100 Subject: [PATCH] implements PLN-0053 --- .../messages/assets/AssetFamilyCatalog.java | 1 + .../messages/assets/OutputFormatCatalog.java | 1 + .../repositories/PackerAssetWalker.java | 3 + .../FileSystemPackerWorkspaceService.java | 17 +++ .../PackerAssetDeclarationParser.java | 2 +- .../FileSystemPackerWorkspaceServiceTest.java | 23 ++++ .../PackerAssetDeclarationParserTest.java | 25 +++++ .../java/p/studio/utilities/i18n/I18n.java | 11 ++ .../assets/details/AssetDetailsControl.java | 20 +++- .../assets/details/AssetDetailsUiSupport.java | 12 ++ .../details/AssetListPackerMappings.java | 2 + .../bank/AssetBankCapacityService.java | 6 + .../summary/AssetDetailsSummaryControl.java | 35 +++++- .../assets/list/AssetListItemControl.java | 2 + .../messages/AssetWorkspaceAssetDetails.java | 2 + .../messages/AssetWorkspaceAssetSummary.java | 3 + .../AssetStudioGlyphSpecialization.java | 33 ++++++ .../metadata/AssetStudioMetadataService.java | 104 ++++++++++++++++++ .../metadata/AssetStudioMetadataSnapshot.java | 12 ++ .../AssetStudioSceneBankMetadata.java | 22 ++++ .../AssetStudioSceneLayerBinding.java | 15 +++ .../assets/wizards/AddAssetWizard.java | 58 ++++++++++ .../main/resources/i18n/messages.properties | 11 ++ ...DetailsBankCompositionCoordinatorTest.java | 4 + ...ailsPaletteOverhaulingCoordinatorTest.java | 3 + .../AssetStudioMetadataServiceTest.java | 76 +++++++++++++ 26 files changed, 498 insertions(+), 5 deletions(-) create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioGlyphSpecialization.java create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioMetadataService.java create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioMetadataSnapshot.java create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioSceneBankMetadata.java create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioSceneLayerBinding.java create mode 100644 prometeu-studio/src/test/java/p/studio/workspaces/assets/metadata/AssetStudioMetadataServiceTest.java diff --git a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/assets/AssetFamilyCatalog.java b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/assets/AssetFamilyCatalog.java index 4ae04333..3271ce0a 100644 --- a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/assets/AssetFamilyCatalog.java +++ b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/assets/AssetFamilyCatalog.java @@ -4,6 +4,7 @@ import java.util.Locale; public enum AssetFamilyCatalog { GLYPH_BANK("glyph_bank"), + SCENE_BANK("scene_bank"), SOUND_BANK("sound_bank"), UNKNOWN("unknown"); diff --git a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/assets/OutputFormatCatalog.java b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/assets/OutputFormatCatalog.java index 5b5904b8..7ac4aa87 100644 --- a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/assets/OutputFormatCatalog.java +++ b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/assets/OutputFormatCatalog.java @@ -5,6 +5,7 @@ import java.util.Locale; public enum OutputFormatCatalog { 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"), UNKNOWN(AssetFamilyCatalog.UNKNOWN, "unknown", "Unknown"); diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/repositories/PackerAssetWalker.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/repositories/PackerAssetWalker.java index da9d0e10..309a9870 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/repositories/PackerAssetWalker.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/repositories/PackerAssetWalker.java @@ -100,6 +100,9 @@ public class PackerAssetWalker { diagnostics.addAll(walkResult.diagnostics()); return new PackerWalkResult(walkResult.probeResults(), diagnostics); } + case SCENE_BANK -> { + return new PackerWalkResult(List.of(), diagnostics); + } case UNKNOWN -> { diagnostics.add(new PackerDiagnostic( PackerDiagnosticSeverity.WARNING, diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java index df8335d2..f9f464c5 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java @@ -38,6 +38,7 @@ import java.util.stream.Stream; public final class FileSystemPackerWorkspaceService implements PackerWorkspaceService { 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 PackerWorkspaceFoundation workspaceFoundation; @@ -287,6 +288,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe final PackerRegistryEntry entry = workspaceFoundation.allocateIdentity(project, registry, assetRoot); Files.createDirectories(assetRoot); writeManifest(manifestPath, request, entry.assetUuid()); + writeStudioSupportFiles(assetRoot, request); final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry); workspaceFoundation.saveRegistry(project, updated); 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); } + private void writeStudioSupportFiles(Path assetRoot, CreateAssetRequest request) throws IOException { + if (request.assetFamily() != AssetFamilyCatalog.SCENE_BANK) { + return; + } + final Map 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) { final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/'); if (raw.isBlank()) { diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDeclarationParser.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDeclarationParser.java index 1e8ab3a5..fbe08661 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDeclarationParser.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDeclarationParser.java @@ -121,7 +121,7 @@ public final class PackerAssetDeclarationParser { diagnostics.add(new PackerDiagnostic( PackerDiagnosticSeverity.ERROR, 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, true)); return null; diff --git a/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/FileSystemPackerWorkspaceServiceTest.java b/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/FileSystemPackerWorkspaceServiceTest.java index 6311b4c9..07a0d29b 100644 --- a/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/FileSystemPackerWorkspaceServiceTest.java +++ b/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/FileSystemPackerWorkspaceServiceTest.java @@ -512,6 +512,29 @@ final class FileSystemPackerWorkspaceServiceTest { 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 void returnsCreatedAssetThroughRuntimeBackedDetailsWithoutRescanMismatch() throws Exception { final Path projectRoot = tempDir.resolve("created-details"); diff --git a/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/PackerAssetDeclarationParserTest.java b/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/PackerAssetDeclarationParserTest.java index c650a87a..4b81aaaa 100644 --- a/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/PackerAssetDeclarationParserTest.java +++ b/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/PackerAssetDeclarationParserTest.java @@ -67,6 +67,31 @@ final class PackerAssetDeclarationParserTest { 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 void rejectsNonObjectPipelineMetadata() throws Exception { final Path manifest = tempDir.resolve("asset.json"); diff --git a/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java b/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java index beaf422e..acd59d23 100644 --- a/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java +++ b/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java @@ -190,10 +190,19 @@ public enum I18n { ASSETS_LABEL_BUILD_PARTICIPATION("assets.label.buildParticipation"), ASSETS_LABEL_ASSET_ID("assets.label.assetId"), 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_SCENE_BANK("assets.type.sceneBank"), ASSETS_TYPE_PALETTE_BANK("assets.type.paletteBank"), ASSETS_TYPE_SOUND_BANK("assets.type.soundBank"), 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_BANK("assets.label.bank"), 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_ROOT("assets.addWizard.label.root"), 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_CODEC("assets.addWizard.label.codec"), ASSETS_ADD_WIZARD_LABEL_PRELOAD("assets.addWizard.label.preload"), 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_CODEC("assets.addWizard.prompt.codec"), ASSETS_ADD_WIZARD_ASSETS_ROOT_HINT("assets.addWizard.assetsRootHint"), diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java index 504902fb..8a1c24f1 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java @@ -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.palette.AssetDetailsPaletteOverhaulingControl; 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.AssetWorkspaceBankCompositionDetails; 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 AssetDetailsBankCompositionControl bankCompositionControl; private final AssetDetailsPaletteOverhaulingControl paletteOverhaulingControl; + private final AssetStudioMetadataService studioMetadataService = new AssetStudioMetadataService(); private final VBox actionsContent = new VBox(10); private final ScrollPane actionsScroll = new ScrollPane(); private final VBox actionsSection; @@ -534,6 +537,10 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware PackerAssetDetailsDTO details, java.util.List diagnostics, java.util.List actions) { + final var baseSummary = AssetListPackerMappings.mapSummary(details.summary()); + final AssetStudioMetadataSnapshot studioMetadata = studioMetadataService.read( + baseSummary.assetRoot(), + baseSummary.assetFamily()); final java.util.List mergedDiagnostics = new java.util.ArrayList<>(details.diagnostics()); for (PackerDiagnosticDTO diagnostic : diagnostics) { if (!mergedDiagnostics.contains(diagnostic)) { @@ -541,7 +548,17 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware } } 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() .map(action -> new AssetWorkspaceAssetAction( action.action(), @@ -557,6 +574,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware details.outputPipeline(), mapBankComposition(details.bankComposition()), details.pipelinePalettes(), + studioMetadata.sceneBankMetadata(), mergedDiagnostics); } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsUiSupport.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsUiSupport.java index ada19cd0..0d01bc2b 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsUiSupport.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsUiSupport.java @@ -16,6 +16,7 @@ import p.studio.controls.forms.StudioFormSection; import p.studio.controls.forms.StudioSection; import p.studio.projects.ProjectReference; 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.AssetWorkspaceBuildParticipation; @@ -114,6 +115,7 @@ public final class AssetDetailsUiSupport { public static String typeLabel(AssetFamilyCatalog assetFamily) { return switch (assetFamily) { 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 UNKNOWN -> Container.i18n().text(I18n.ASSETS_TYPE_UNKNOWN); }; @@ -122,11 +124,21 @@ public final class AssetDetailsUiSupport { public static String typeChipTone(AssetFamilyCatalog assetFamily) { return switch (assetFamily) { case GLYPH_BANK -> "assets-details-chip-image"; + case SCENE_BANK -> "assets-details-chip-generic"; case SOUND_BANK -> "assets-details-chip-audio"; 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) { return switch (action) { case REGISTER -> Container.i18n().text(I18n.ASSETS_ACTION_REGISTER); diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetListPackerMappings.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetListPackerMappings.java index 3e77d669..168f3e6a 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetListPackerMappings.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetListPackerMappings.java @@ -1,6 +1,7 @@ package p.studio.workspaces.assets.details; 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.AssetWorkspaceAssetSummary; import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation; @@ -25,6 +26,7 @@ public final class AssetListPackerMappings { buildParticipation, summary.identity().assetId(), summary.assetFamily(), + AssetStudioGlyphSpecialization.NONE, summary.identity().assetRoot(), summary.preloadEnabled(), summary.hasDiagnostics()); diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetBankCapacityService.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetBankCapacityService.java index 10415592..64bc922b 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetBankCapacityService.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetBankCapacityService.java @@ -21,6 +21,12 @@ public final class AssetBankCapacityService { final Map safePipeline = Map.copyOf(Objects.requireNonNull(outputPipeline, "outputPipeline")); return switch (safeFamily) { 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 UNKNOWN -> new AssetDetailsBankCompositionCapacityState( 0.0d, diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/summary/AssetDetailsSummaryControl.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/summary/AssetDetailsSummaryControl.java index ce1bad59..52128a88 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/summary/AssetDetailsSummaryControl.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/summary/AssetDetailsSummaryControl.java @@ -8,6 +8,9 @@ import p.studio.lsp.events.StudioWorkspaceEventBus; import p.studio.projects.ProjectReference; import p.studio.utilities.i18n.I18n; 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.AssetWorkspaceDetailsViewState; 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 AssetStudioSceneBankMetadata sceneBankMetadata = viewState.selectedAssetDetails().sceneBankMetadata(); 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_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( Container.i18n().text(I18n.ASSETS_LABEL_TYPE), - AssetDetailsUiSupport.createChip( - AssetDetailsUiSupport.typeChipTone(summary.assetFamily()), - AssetDetailsUiSupport.typeLabel(summary.assetFamily())))); + typeBox)); + if (summary.glyphSpecialization() != AssetStudioGlyphSpecialization.NONE) { + 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( Container.i18n().text(I18n.ASSETS_LABEL_REGISTRATION), AssetDetailsUiSupport.createChip( diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/list/AssetListItemControl.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/list/AssetListItemControl.java index 06a4aae2..70365dc4 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/list/AssetListItemControl.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/list/AssetListItemControl.java @@ -113,6 +113,7 @@ public final class AssetListItemControl extends VBox { private String assetRowToneClass(AssetFamilyCatalog assetFamily) { return switch (assetFamily) { 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"; default -> "assets-workspace-asset-row-tone-generic"; }; @@ -121,6 +122,7 @@ public final class AssetListItemControl extends VBox { private String assetNameToneClass(AssetFamilyCatalog assetFamily) { return switch (assetFamily) { 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"; default -> "assets-workspace-asset-name-tone-generic"; }; diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetDetails.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetDetails.java index d6c3cf43..7a1d42c0 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetDetails.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetDetails.java @@ -4,6 +4,7 @@ import p.packer.dtos.PackerCodecConfigurationFieldDTO; import p.packer.dtos.PackerDiagnosticDTO; import p.packer.messages.assets.OutputCodecCatalog; import p.packer.messages.assets.OutputFormatCatalog; +import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata; import java.util.List; import java.util.Map; @@ -20,6 +21,7 @@ public record AssetWorkspaceAssetDetails( Map outputPipeline, AssetWorkspaceBankCompositionDetails bankComposition, List> pipelinePalettes, + AssetStudioSceneBankMetadata sceneBankMetadata, List diagnostics) { public AssetWorkspaceAssetDetails { diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetSummary.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetSummary.java index af445af7..bca9b9ca 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetSummary.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetSummary.java @@ -2,6 +2,7 @@ package p.studio.workspaces.assets.messages; import p.packer.messages.AssetReference; import p.packer.messages.assets.AssetFamilyCatalog; +import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization; import java.nio.file.Path; import java.util.Objects; @@ -13,6 +14,7 @@ public record AssetWorkspaceAssetSummary( AssetWorkspaceBuildParticipation buildParticipation, Integer assetId, AssetFamilyCatalog assetFamily, + AssetStudioGlyphSpecialization glyphSpecialization, Path assetRoot, boolean preload, boolean hasDiagnostics) { @@ -23,6 +25,7 @@ public record AssetWorkspaceAssetSummary( Objects.requireNonNull(state, "state"); Objects.requireNonNull(buildParticipation, "buildParticipation"); assetFamily = Objects.requireNonNullElse(assetFamily, AssetFamilyCatalog.UNKNOWN); + glyphSpecialization = Objects.requireNonNullElse(glyphSpecialization, AssetStudioGlyphSpecialization.NONE); assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize(); if (assetName.isBlank()) { throw new IllegalArgumentException("assetName must not be blank"); diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioGlyphSpecialization.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioGlyphSpecialization.java new file mode 100644 index 00000000..1115d28d --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioGlyphSpecialization.java @@ -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; + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioMetadataService.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioMetadataService.java new file mode 100644 index 00000000..9792708d --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioMetadataService.java @@ -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 bindings = new ArrayList<>(); + final Set 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; + } + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioMetadataSnapshot.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioMetadataSnapshot.java new file mode 100644 index 00000000..07d196fa --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioMetadataSnapshot.java @@ -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); + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioSceneBankMetadata.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioSceneBankMetadata.java new file mode 100644 index 00000000..cf74536f --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioSceneBankMetadata.java @@ -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 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"); + } + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioSceneLayerBinding.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioSceneLayerBinding.java new file mode 100644 index 00000000..1eecc4dd --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioSceneLayerBinding.java @@ -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"); + } + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/wizards/AddAssetWizard.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/wizards/AddAssetWizard.java index f32b2b82..a8d8cd32 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/wizards/AddAssetWizard.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/wizards/AddAssetWizard.java @@ -24,6 +24,8 @@ import p.studio.Container; import p.studio.projects.ProjectReference; import p.studio.utilities.i18n.I18n; 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.IOException; @@ -47,9 +49,11 @@ public final class AddAssetWizard { private final TextField assetRootField = new TextField(); private final TextField assetNameField = new TextField(); private final ComboBox assetFamilyCombo = new ComboBox<>(); + private final ComboBox glyphSpecializationCombo = new ComboBox<>(); private final ComboBox outputFormatCombo = new ComboBox<>(); private final ComboBox outputCodecCombo = new ComboBox<>(); private final CheckBox preloadCheckBox = new CheckBox(); + private final AssetStudioMetadataService studioMetadataService = new AssetStudioMetadataService(); private int stepIndex; private boolean creating; @@ -65,6 +69,7 @@ public final class AddAssetWizard { preloadCheckBox.setSelected(false); configureAssetFamilyCombo(); + configureGlyphSpecializationCombo(); configureOutputFormatCombo(); configureOutputCodecCombo(); renderStep(); @@ -131,7 +136,34 @@ public final class AddAssetWizard { }); assetFamilyCombo.valueProperty().addListener((ignored, oldValue, newValue) -> { if (!Objects.equals(oldValue, newValue)) { + if (newValue != AssetFamilyCatalog.GLYPH_BANK) { + glyphSpecializationCombo.getSelectionModel().select(AssetStudioGlyphSpecialization.NONE); + } 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 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 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 noteLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_NOTE)); noteLabel.setWrapText(true); 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(""); stepBody.getChildren().setAll( new VBox(6, nameLabel, assetNameField), new VBox(6, typeLabel, assetFamilyCombo), + specializationBox, new VBox(6, formatLabel, outputFormatCombo), new VBox(6, codecLabel, outputCodecCombo), new VBox(6, preloadLabel, preloadCheckBox), @@ -360,6 +398,15 @@ public final class AddAssetWizard { private void applyCreateResult(CreateAssetResult createResult) { creating = false; 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()); stage.close(); return; @@ -409,6 +456,17 @@ public final class AddAssetWizard { 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) { final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/'); if (raw.isBlank()) { diff --git a/prometeu-studio/src/main/resources/i18n/messages.properties b/prometeu-studio/src/main/resources/i18n/messages.properties index d3f0dd0e..ed96d566 100644 --- a/prometeu-studio/src/main/resources/i18n/messages.properties +++ b/prometeu-studio/src/main/resources/i18n/messages.properties @@ -181,10 +181,19 @@ assets.label.registration=Registration assets.label.buildParticipation=Build Participation assets.label.assetId=Asset ID 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.sceneBank=Scene Bank assets.type.paletteBank=Palette Bank assets.type.soundBank=Sound Bank 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.bank=Bank 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.root=Asset Root assets.addWizard.label.type=Asset Type +assets.addWizard.label.specialization=Studio Specialization assets.addWizard.label.format=Output Format assets.addWizard.label.codec=Output Codec assets.addWizard.label.preload=Preload on startup assets.addWizard.prompt.type=Choose asset type +assets.addWizard.prompt.specialization=Choose Studio specialization assets.addWizard.prompt.format=Choose output format assets.addWizard.prompt.codec=Choose output codec assets.addWizard.assetsRootHint=Assets root: {0} diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionCoordinatorTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionCoordinatorTest.java index b632b9e9..1b0c470f 100644 --- a/prometeu-studio/src/test/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionCoordinatorTest.java +++ b/prometeu-studio/src/test/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionCoordinatorTest.java @@ -7,6 +7,7 @@ import p.packer.messages.assets.AssetFamilyCatalog; import p.packer.messages.assets.OutputCodecCatalog; import p.packer.messages.assets.OutputFormatCatalog; import p.packer.messages.assets.PackerCodecConfigurationFieldType; +import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization; import p.studio.workspaces.assets.messages.*; import java.nio.file.Path; @@ -114,6 +115,7 @@ final class AssetDetailsBankCompositionCoordinatorTest { List.of(), 0L), List.of(), + null, List.of()); } @@ -136,6 +138,7 @@ final class AssetDetailsBankCompositionCoordinatorTest { List.of(), 0L), List.of(), + null, List.of()); } @@ -159,6 +162,7 @@ final class AssetDetailsBankCompositionCoordinatorTest { AssetWorkspaceBuildParticipation.INCLUDED, 1, family, + AssetStudioGlyphSpecialization.NONE, Path.of("/tmp/bank"), false, false); diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/assets/details/palette/AssetDetailsPaletteOverhaulingCoordinatorTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/assets/details/palette/AssetDetailsPaletteOverhaulingCoordinatorTest.java index e8e3bc54..90328eb8 100644 --- a/prometeu-studio/src/test/java/p/studio/workspaces/assets/details/palette/AssetDetailsPaletteOverhaulingCoordinatorTest.java +++ b/prometeu-studio/src/test/java/p/studio/workspaces/assets/details/palette/AssetDetailsPaletteOverhaulingCoordinatorTest.java @@ -5,6 +5,7 @@ import p.packer.messages.AssetReference; import p.packer.messages.assets.AssetFamilyCatalog; import p.packer.messages.assets.OutputCodecCatalog; import p.packer.messages.assets.OutputFormatCatalog; +import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization; import p.studio.workspaces.assets.messages.*; import java.nio.file.Path; @@ -81,6 +82,7 @@ final class AssetDetailsPaletteOverhaulingCoordinatorTest { AssetWorkspaceBuildParticipation.INCLUDED, 1, AssetFamilyCatalog.GLYPH_BANK, + AssetStudioGlyphSpecialization.NONE, Path.of("/tmp/bank"), false, false), @@ -93,6 +95,7 @@ final class AssetDetailsPaletteOverhaulingCoordinatorTest { Map.of(), new AssetWorkspaceBankCompositionDetails(availableFiles, selectedFiles, 0L), selectedFiles.stream().map(file -> (Map) file.metadata().get("palette")).toList(), + null, List.of()); } diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/assets/metadata/AssetStudioMetadataServiceTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/assets/metadata/AssetStudioMetadataServiceTest.java new file mode 100644 index 00000000..51301395 --- /dev/null +++ b/prometeu-studio/src/test/java/p/studio/workspaces/assets/metadata/AssetStudioMetadataServiceTest.java @@ -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()); + } +}