dev/studio-tiled-parser-assets-scene-asset-type #4

Open
bquarkz wants to merge 6 commits from dev/studio-tiled-parser-assets-scene-asset-type into master
26 changed files with 498 additions and 5 deletions
Showing only changes of commit bcc5b413fa - Show all commits

View File

@ -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");

View File

@ -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");

View File

@ -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,

View File

@ -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<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) {
final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/');
if (raw.isBlank()) {

View File

@ -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;

View File

@ -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");

View File

@ -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");

View File

@ -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"),

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.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<PackerDiagnosticDTO> diagnostics,
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());
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);
}

View File

@ -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);

View File

@ -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());

View File

@ -21,6 +21,12 @@ public final class AssetBankCapacityService {
final Map<String, Object> 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,

View File

@ -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(

View File

@ -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";
};

View File

@ -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<String, Object> outputPipeline,
AssetWorkspaceBankCompositionDetails bankComposition,
List<Map<String, Object>> pipelinePalettes,
AssetStudioSceneBankMetadata sceneBankMetadata,
List<PackerDiagnosticDTO> diagnostics) {
public AssetWorkspaceAssetDetails {

View File

@ -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");

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.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<AssetFamilyCatalog> assetFamilyCombo = new ComboBox<>();
private final ComboBox<AssetStudioGlyphSpecialization> glyphSpecializationCombo = new ComboBox<>();
private final ComboBox<OutputFormatCatalog> outputFormatCombo = new ComboBox<>();
private final ComboBox<OutputCodecCatalog> 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()) {

View File

@ -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}

View File

@ -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);

View File

@ -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<String, Object>) file.metadata().get("palette")).toList(),
null,
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());
}
}