Compare commits
4 Commits
ea8c81368b
...
a6b0760657
| Author | SHA1 | Date | |
|---|---|---|---|
| a6b0760657 | |||
| b77a24c57b | |||
| 831e419c80 | |||
| bbd588eed4 |
@ -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");
|
||||||
|
|
||||||
|
|||||||
@ -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");
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,25 @@ 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("map_width", 16);
|
||||||
|
supportFile.put("map_height", 16);
|
||||||
|
supportFile.put("layer_count", 1);
|
||||||
|
supportFile.put("layers", List.of(Map.of(
|
||||||
|
"index", 1,
|
||||||
|
"name", "Layer 1",
|
||||||
|
"tilemap", "layer-1.tmx",
|
||||||
|
"tileset_asset_root", "")));
|
||||||
|
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()) {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -512,6 +512,32 @@ 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(16, supportFile.path("map_width").asInt());
|
||||||
|
assertEquals(16, supportFile.path("map_height").asInt());
|
||||||
|
assertEquals(1, supportFile.path("layer_count").asInt());
|
||||||
|
assertEquals("Layer 1", supportFile.path("layers").get(0).path("name").asText());
|
||||||
|
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");
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -154,6 +154,10 @@ public enum I18n {
|
|||||||
ASSETS_ACTIONS_EMPTY("assets.actions.empty"),
|
ASSETS_ACTIONS_EMPTY("assets.actions.empty"),
|
||||||
ASSETS_ACTION_REGISTER("assets.action.register"),
|
ASSETS_ACTION_REGISTER("assets.action.register"),
|
||||||
ASSETS_ACTION_ANALYSE("assets.action.analyse"),
|
ASSETS_ACTION_ANALYSE("assets.action.analyse"),
|
||||||
|
ASSETS_ACTION_GENERATE_TSX("assets.action.generateTsx"),
|
||||||
|
ASSETS_ACTION_GENERATE_TMX("assets.action.generateTmx"),
|
||||||
|
ASSETS_ACTION_VALIDATE_SCENE_BANK("assets.action.validateSceneBank"),
|
||||||
|
ASSETS_ACTION_ACCEPT_SCENE_BANK("assets.action.acceptSceneBank"),
|
||||||
ASSETS_ACTION_DELETE("assets.action.delete"),
|
ASSETS_ACTION_DELETE("assets.action.delete"),
|
||||||
ASSETS_ACTION_INCLUDE_IN_BUILD("assets.action.includeInBuild"),
|
ASSETS_ACTION_INCLUDE_IN_BUILD("assets.action.includeInBuild"),
|
||||||
ASSETS_ACTION_EXCLUDE_FROM_BUILD("assets.action.excludeFromBuild"),
|
ASSETS_ACTION_EXCLUDE_FROM_BUILD("assets.action.excludeFromBuild"),
|
||||||
@ -190,10 +194,24 @@ 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_STATUS("assets.label.sceneStatus"),
|
||||||
|
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_SCENE_STATUS_PENDING_VALIDATION("assets.sceneStatus.pendingValidation"),
|
||||||
|
ASSETS_SCENE_STATUS_VALIDATED_PENDING_ACCEPTANCE("assets.sceneStatus.validatedPendingAcceptance"),
|
||||||
|
ASSETS_SCENE_STATUS_READY("assets.sceneStatus.ready"),
|
||||||
|
ASSETS_SCENE_STATUS_VALIDATION_FAILED("assets.sceneStatus.validationFailed"),
|
||||||
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 +272,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"),
|
||||||
|
|||||||
@ -23,21 +23,31 @@ 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.metadata.AssetStudioGlyphSpecialization;
|
||||||
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;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
||||||
|
import p.studio.workspaces.assets.messages.AssetWorkspaceSceneBankValidation;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsStatus;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsStatus;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation;
|
||||||
|
import p.studio.workspaces.assets.messages.events.StudioAssetLogEvent;
|
||||||
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
||||||
import p.studio.workspaces.assets.messages.events.StudioAssetsRefreshRequestedEvent;
|
import p.studio.workspaces.assets.messages.events.StudioAssetsRefreshRequestedEvent;
|
||||||
import p.studio.workspaces.assets.messages.events.StudioAssetsWorkspaceSelectionRequestedEvent;
|
import p.studio.workspaces.assets.messages.events.StudioAssetsWorkspaceSelectionRequestedEvent;
|
||||||
|
import p.studio.workspaces.assets.scene.SceneBankWorkflowResult;
|
||||||
|
import p.studio.workspaces.assets.scene.SceneBankWorkflowService;
|
||||||
|
import p.studio.workspaces.assets.tiled.TiledAssetGenerationResult;
|
||||||
|
import p.studio.workspaces.assets.tiled.TiledAssetGenerationService;
|
||||||
import p.studio.workspaces.assets.wizards.DeleteAssetDialog;
|
import p.studio.workspaces.assets.wizards.DeleteAssetDialog;
|
||||||
import p.studio.workspaces.assets.wizards.MoveAssetWizard;
|
import p.studio.workspaces.assets.wizards.MoveAssetWizard;
|
||||||
import p.studio.workspaces.framework.StudioEventAware;
|
import p.studio.workspaces.framework.StudioEventAware;
|
||||||
import p.studio.workspaces.framework.StudioEventBindings;
|
import p.studio.workspaces.framework.StudioEventBindings;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
@ -53,6 +63,9 @@ 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 TiledAssetGenerationService tiledGenerationService = new TiledAssetGenerationService();
|
||||||
|
private final SceneBankWorkflowService sceneBankWorkflowService = new SceneBankWorkflowService();
|
||||||
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;
|
||||||
@ -324,6 +337,29 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
analyseButton.setDisable(actionRunning || viewState.selectedAssetDetails().diagnostics().isEmpty());
|
analyseButton.setDisable(actionRunning || viewState.selectedAssetDetails().diagnostics().isEmpty());
|
||||||
analyseButton.setOnAction(ignored -> openDiagnosticsDialog());
|
analyseButton.setOnAction(ignored -> openDiagnosticsDialog());
|
||||||
nodes.add(analyseButton);
|
nodes.add(analyseButton);
|
||||||
|
if (canGenerateTsx()) {
|
||||||
|
final Button generateTsxButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_GENERATE_TSX));
|
||||||
|
generateTsxButton.setDisable(actionRunning);
|
||||||
|
generateTsxButton.setOnAction(ignored -> generateTsx());
|
||||||
|
nodes.add(generateTsxButton);
|
||||||
|
}
|
||||||
|
if (canGenerateTmx()) {
|
||||||
|
final Button generateTmxButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_GENERATE_TMX));
|
||||||
|
generateTmxButton.setDisable(actionRunning);
|
||||||
|
generateTmxButton.setOnAction(ignored -> generateTmx());
|
||||||
|
nodes.add(generateTmxButton);
|
||||||
|
}
|
||||||
|
if (canValidateSceneBank()) {
|
||||||
|
final Button validateSceneButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_VALIDATE_SCENE_BANK));
|
||||||
|
validateSceneButton.setDisable(actionRunning);
|
||||||
|
validateSceneButton.setOnAction(ignored -> validateSceneBank());
|
||||||
|
nodes.add(validateSceneButton);
|
||||||
|
|
||||||
|
final Button acceptSceneButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_ACCEPT_SCENE_BANK));
|
||||||
|
acceptSceneButton.setDisable(actionRunning || !viewState.selectedAssetDetails().sceneBankValidation().canAccept());
|
||||||
|
acceptSceneButton.setOnAction(ignored -> acceptSceneBank());
|
||||||
|
nodes.add(acceptSceneButton);
|
||||||
|
}
|
||||||
|
|
||||||
final Button buildParticipationButton = AssetDetailsUiSupport.createActionButton(buildParticipationActionLabel());
|
final Button buildParticipationButton = AssetDetailsUiSupport.createActionButton(buildParticipationActionLabel());
|
||||||
AssetDetailsUiSupport.applyActionTone(
|
AssetDetailsUiSupport.applyActionTone(
|
||||||
@ -449,6 +485,100 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean canGenerateTsx() {
|
||||||
|
return viewState.selectedAssetDetails() != null
|
||||||
|
&& viewState.selectedAssetDetails().summary().assetFamily() == p.packer.messages.assets.AssetFamilyCatalog.GLYPH_BANK
|
||||||
|
&& viewState.selectedAssetDetails().summary().glyphSpecialization() == AssetStudioGlyphSpecialization.TILESET;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canGenerateTmx() {
|
||||||
|
return viewState.selectedAssetDetails() != null
|
||||||
|
&& viewState.selectedAssetDetails().summary().assetFamily() == p.packer.messages.assets.AssetFamilyCatalog.SCENE_BANK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canValidateSceneBank() {
|
||||||
|
return viewState.selectedAssetDetails() != null
|
||||||
|
&& viewState.selectedAssetDetails().summary().assetFamily() == p.packer.messages.assets.AssetFamilyCatalog.SCENE_BANK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateTsx() {
|
||||||
|
if (actionRunning || viewState.selectedAssetDetails() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
actionRunning = true;
|
||||||
|
actionFeedbackMessage = null;
|
||||||
|
renderActions();
|
||||||
|
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||||
|
Container.backgroundTasks().submit(() -> {
|
||||||
|
final TiledAssetGenerationResult result = tiledGenerationService.generateTilesetTsx(details);
|
||||||
|
Platform.runLater(() -> applyTiledGenerationResult(result));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateTmx() {
|
||||||
|
if (actionRunning || viewState.selectedAssetDetails() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
actionRunning = true;
|
||||||
|
actionFeedbackMessage = null;
|
||||||
|
renderActions();
|
||||||
|
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||||
|
Container.backgroundTasks().submit(() -> {
|
||||||
|
final TiledAssetGenerationResult result = tiledGenerationService.generateSceneBankTilemaps(projectReference, details);
|
||||||
|
Platform.runLater(() -> applyTiledGenerationResult(result));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyTiledGenerationResult(TiledAssetGenerationResult result) {
|
||||||
|
actionRunning = false;
|
||||||
|
actionFeedbackMessage = result.message();
|
||||||
|
workspaceBus.publish(new StudioAssetLogEvent("scene-bank", result.message()));
|
||||||
|
renderActions();
|
||||||
|
if (result.success() && viewState.selectedAssetReference() != null) {
|
||||||
|
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(viewState.selectedAssetReference()));
|
||||||
|
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(viewState.selectedAssetReference(), true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateSceneBank() {
|
||||||
|
if (actionRunning || viewState.selectedAssetDetails() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
actionRunning = true;
|
||||||
|
actionFeedbackMessage = null;
|
||||||
|
renderActions();
|
||||||
|
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||||
|
Container.backgroundTasks().submit(() -> {
|
||||||
|
final SceneBankWorkflowResult result = sceneBankWorkflowService.validate(projectReference, details);
|
||||||
|
Platform.runLater(() -> applySceneBankWorkflowResult("scene-bank-validate", result));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acceptSceneBank() {
|
||||||
|
if (actionRunning || viewState.selectedAssetDetails() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
actionRunning = true;
|
||||||
|
actionFeedbackMessage = null;
|
||||||
|
renderActions();
|
||||||
|
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||||
|
Container.backgroundTasks().submit(() -> {
|
||||||
|
final SceneBankWorkflowResult result = sceneBankWorkflowService.accept(projectReference, details);
|
||||||
|
Platform.runLater(() -> applySceneBankWorkflowResult("scene-bank-accept", result));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applySceneBankWorkflowResult(String source, SceneBankWorkflowResult result) {
|
||||||
|
actionRunning = false;
|
||||||
|
actionFeedbackMessage = result.message();
|
||||||
|
workspaceBus.publish(new StudioAssetLogEvent(source, result.message()));
|
||||||
|
renderActions();
|
||||||
|
if (viewState.selectedAssetReference() != null) {
|
||||||
|
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(viewState.selectedAssetReference()));
|
||||||
|
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(viewState.selectedAssetReference(), true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void applyBuildParticipationResult(UpdateAssetBuildParticipationResult result) {
|
private void applyBuildParticipationResult(UpdateAssetBuildParticipationResult result) {
|
||||||
actionRunning = false;
|
actionRunning = false;
|
||||||
if (result.status() == p.packer.messages.PackerOperationStatus.SUCCESS && result.assetReference() != null) {
|
if (result.status() == p.packer.messages.PackerOperationStatus.SUCCESS && result.assetReference() != null) {
|
||||||
@ -534,29 +664,69 @@ 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 java.util.List<PackerDiagnosticDTO> mergedDiagnostics = new java.util.ArrayList<>(details.diagnostics());
|
final var baseSummary = AssetListPackerMappings.mapSummary(details.summary());
|
||||||
for (PackerDiagnosticDTO diagnostic : diagnostics) {
|
final AssetStudioMetadataSnapshot studioMetadata = studioMetadataService.read(
|
||||||
if (!mergedDiagnostics.contains(diagnostic)) {
|
baseSummary.assetRoot(),
|
||||||
mergedDiagnostics.add(diagnostic);
|
baseSummary.assetFamily());
|
||||||
}
|
final java.util.List<AssetWorkspaceAssetAction> mappedActions = actions.stream()
|
||||||
}
|
.map(action -> new AssetWorkspaceAssetAction(
|
||||||
return new AssetWorkspaceAssetDetails(
|
action.action(),
|
||||||
AssetListPackerMappings.mapSummary(details.summary()),
|
action.enabled(),
|
||||||
actions.stream()
|
action.visible(),
|
||||||
.map(action -> new AssetWorkspaceAssetAction(
|
action.reason()))
|
||||||
action.action(),
|
.toList();
|
||||||
action.enabled(),
|
final p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary mappedSummary =
|
||||||
action.visible(),
|
new p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary(
|
||||||
action.reason()))
|
baseSummary.assetReference(),
|
||||||
.toList(),
|
baseSummary.assetName(),
|
||||||
|
baseSummary.state(),
|
||||||
|
baseSummary.buildParticipation(),
|
||||||
|
baseSummary.assetId(),
|
||||||
|
baseSummary.assetFamily(),
|
||||||
|
studioMetadata.glyphSpecialization(),
|
||||||
|
baseSummary.assetRoot(),
|
||||||
|
baseSummary.preload(),
|
||||||
|
baseSummary.hasDiagnostics());
|
||||||
|
final AssetWorkspaceBankCompositionDetails bankComposition = mapBankComposition(details.bankComposition());
|
||||||
|
final AssetWorkspaceAssetDetails draftDetails = new AssetWorkspaceAssetDetails(
|
||||||
|
mappedSummary,
|
||||||
|
mappedActions,
|
||||||
details.outputFormat(),
|
details.outputFormat(),
|
||||||
details.outputCodec(),
|
details.outputCodec(),
|
||||||
details.availableOutputCodecs(),
|
details.availableOutputCodecs(),
|
||||||
details.codecConfigurationFieldsByCodec(),
|
details.codecConfigurationFieldsByCodec(),
|
||||||
details.metadataFields(),
|
details.metadataFields(),
|
||||||
details.outputPipeline(),
|
details.outputPipeline(),
|
||||||
mapBankComposition(details.bankComposition()),
|
bankComposition,
|
||||||
details.pipelinePalettes(),
|
details.pipelinePalettes(),
|
||||||
|
studioMetadata.sceneBankMetadata(),
|
||||||
|
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||||
|
List.of());
|
||||||
|
final SceneBankWorkflowResult sceneBankWorkflow = sceneBankWorkflowService.inspect(projectReference, draftDetails);
|
||||||
|
final java.util.List<PackerDiagnosticDTO> mergedDiagnostics = new java.util.ArrayList<>(details.diagnostics());
|
||||||
|
for (PackerDiagnosticDTO diagnostic : diagnostics) {
|
||||||
|
if (!mergedDiagnostics.contains(diagnostic)) {
|
||||||
|
mergedDiagnostics.add(diagnostic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (PackerDiagnosticDTO diagnostic : sceneBankWorkflow.diagnostics()) {
|
||||||
|
if (!mergedDiagnostics.contains(diagnostic)) {
|
||||||
|
mergedDiagnostics.add(diagnostic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new AssetWorkspaceAssetDetails(
|
||||||
|
mappedSummary,
|
||||||
|
mappedActions,
|
||||||
|
details.outputFormat(),
|
||||||
|
details.outputCodec(),
|
||||||
|
details.availableOutputCodecs(),
|
||||||
|
details.codecConfigurationFieldsByCodec(),
|
||||||
|
details.metadataFields(),
|
||||||
|
details.outputPipeline(),
|
||||||
|
bankComposition,
|
||||||
|
details.pipelinePalettes(),
|
||||||
|
studioMetadata.sceneBankMetadata(),
|
||||||
|
sceneBankWorkflow.validation(),
|
||||||
mergedDiagnostics);
|
mergedDiagnostics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,8 @@ 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.AssetWorkspaceSceneBankStatus;
|
||||||
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 +116,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 +125,31 @@ 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 sceneBankStatusLabel(AssetWorkspaceSceneBankStatus status) {
|
||||||
|
return switch (status) {
|
||||||
|
case NOT_APPLICABLE -> "";
|
||||||
|
case PENDING_VALIDATION -> Container.i18n().text(I18n.ASSETS_SCENE_STATUS_PENDING_VALIDATION);
|
||||||
|
case VALIDATED_PENDING_ACCEPTANCE -> Container.i18n().text(I18n.ASSETS_SCENE_STATUS_VALIDATED_PENDING_ACCEPTANCE);
|
||||||
|
case READY -> Container.i18n().text(I18n.ASSETS_SCENE_STATUS_READY);
|
||||||
|
case VALIDATION_FAILED -> Container.i18n().text(I18n.ASSETS_SCENE_STATUS_VALIDATION_FAILED);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
@ -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());
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,43 @@ 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_STATUS),
|
||||||
|
AssetDetailsUiSupport.sceneBankStatusLabel(viewState.selectedAssetDetails().sceneBankValidation().status())));
|
||||||
|
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(
|
||||||
|
|||||||
@ -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";
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,8 @@ 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,
|
||||||
|
AssetWorkspaceSceneBankValidation sceneBankValidation,
|
||||||
List<PackerDiagnosticDTO> diagnostics) {
|
List<PackerDiagnosticDTO> diagnostics) {
|
||||||
|
|
||||||
public AssetWorkspaceAssetDetails {
|
public AssetWorkspaceAssetDetails {
|
||||||
@ -33,6 +36,7 @@ public record AssetWorkspaceAssetDetails(
|
|||||||
outputPipeline = Map.copyOf(Objects.requireNonNull(outputPipeline, "outputPipeline"));
|
outputPipeline = Map.copyOf(Objects.requireNonNull(outputPipeline, "outputPipeline"));
|
||||||
bankComposition = Objects.requireNonNull(bankComposition, "bankComposition");
|
bankComposition = Objects.requireNonNull(bankComposition, "bankComposition");
|
||||||
pipelinePalettes = List.copyOf(Objects.requireNonNull(pipelinePalettes, "pipelinePalettes"));
|
pipelinePalettes = List.copyOf(Objects.requireNonNull(pipelinePalettes, "pipelinePalettes"));
|
||||||
|
sceneBankValidation = Objects.requireNonNullElse(sceneBankValidation, AssetWorkspaceSceneBankValidation.NOT_APPLICABLE);
|
||||||
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
|
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
package p.studio.workspaces.assets.messages;
|
||||||
|
|
||||||
|
public enum AssetWorkspaceSceneBankStatus {
|
||||||
|
NOT_APPLICABLE,
|
||||||
|
PENDING_VALIDATION,
|
||||||
|
VALIDATED_PENDING_ACCEPTANCE,
|
||||||
|
READY,
|
||||||
|
VALIDATION_FAILED
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package p.studio.workspaces.assets.messages;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record AssetWorkspaceSceneBankValidation(
|
||||||
|
AssetWorkspaceSceneBankStatus status,
|
||||||
|
boolean pendingExternalChanges,
|
||||||
|
boolean canAccept,
|
||||||
|
String currentFingerprint,
|
||||||
|
String validatedFingerprint,
|
||||||
|
String acceptedFingerprint) {
|
||||||
|
|
||||||
|
public static final AssetWorkspaceSceneBankValidation NOT_APPLICABLE = new AssetWorkspaceSceneBankValidation(
|
||||||
|
AssetWorkspaceSceneBankStatus.NOT_APPLICABLE,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
|
||||||
|
public AssetWorkspaceSceneBankValidation {
|
||||||
|
status = Objects.requireNonNullElse(status, AssetWorkspaceSceneBankStatus.NOT_APPLICABLE);
|
||||||
|
currentFingerprint = Objects.requireNonNullElse(currentFingerprint, "");
|
||||||
|
validatedFingerprint = Objects.requireNonNullElse(validatedFingerprint, "");
|
||||||
|
acceptedFingerprint = Objects.requireNonNullElse(acceptedFingerprint, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
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 mapWidth = root.path("map_width").asInt(0);
|
||||||
|
final int mapHeight = root.path("map_height").asInt(0);
|
||||||
|
final int layerCount = root.path("layer_count").asInt(0);
|
||||||
|
if (mapWidth <= 0 || mapHeight <= 0 || 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 layerName = layerNode.path("name").asText("").trim();
|
||||||
|
final String tilemap = layerNode.path("tilemap").asText("").trim();
|
||||||
|
final String tilesetAssetRoot = layerNode.path("tileset_asset_root").asText("").trim();
|
||||||
|
if (index < 1 || index > layerCount || layerName.isBlank() || tilemap.isBlank() || !indexes.add(index)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
bindings.add(new AssetStudioSceneLayerBinding(index, layerName, tilemap, tilesetAssetRoot));
|
||||||
|
}
|
||||||
|
bindings.sort(Comparator.comparingInt(AssetStudioSceneLayerBinding::index));
|
||||||
|
return new AssetStudioSceneBankMetadata(mapWidth, mapHeight, layerCount, bindings, supportFile);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package p.studio.workspaces.assets.metadata;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record AssetStudioSceneBankMetadata(
|
||||||
|
int mapWidth,
|
||||||
|
int mapHeight,
|
||||||
|
int layerCount,
|
||||||
|
List<AssetStudioSceneLayerBinding> layerBindings,
|
||||||
|
Path supportFile) {
|
||||||
|
|
||||||
|
public AssetStudioSceneBankMetadata {
|
||||||
|
layerBindings = List.copyOf(Objects.requireNonNull(layerBindings, "layerBindings"));
|
||||||
|
supportFile = Objects.requireNonNull(supportFile, "supportFile").toAbsolutePath().normalize();
|
||||||
|
if (mapWidth <= 0 || mapHeight <= 0) {
|
||||||
|
throw new IllegalArgumentException("map dimensions must be positive");
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package p.studio.workspaces.assets.metadata;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record AssetStudioSceneLayerBinding(
|
||||||
|
int index,
|
||||||
|
String layerName,
|
||||||
|
String tilemap,
|
||||||
|
String tilesetAssetRoot) {
|
||||||
|
public AssetStudioSceneLayerBinding {
|
||||||
|
layerName = Objects.requireNonNull(layerName, "layerName").trim();
|
||||||
|
tilemap = Objects.requireNonNull(tilemap, "tilemap").trim();
|
||||||
|
tilesetAssetRoot = Objects.requireNonNull(tilesetAssetRoot, "tilesetAssetRoot").trim();
|
||||||
|
if (index <= 0) {
|
||||||
|
throw new IllegalArgumentException("index must be positive");
|
||||||
|
}
|
||||||
|
if (layerName.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("layerName must not be blank");
|
||||||
|
}
|
||||||
|
if (tilemap.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("tilemap must not be blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package p.studio.workspaces.assets.scene;
|
||||||
|
|
||||||
|
import p.packer.dtos.PackerDiagnosticDTO;
|
||||||
|
import p.studio.workspaces.assets.messages.AssetWorkspaceSceneBankValidation;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record SceneBankWorkflowResult(
|
||||||
|
boolean success,
|
||||||
|
String message,
|
||||||
|
AssetWorkspaceSceneBankValidation validation,
|
||||||
|
List<PackerDiagnosticDTO> diagnostics) {
|
||||||
|
|
||||||
|
public SceneBankWorkflowResult {
|
||||||
|
message = Objects.requireNonNullElse(message, "");
|
||||||
|
validation = Objects.requireNonNullElse(validation, AssetWorkspaceSceneBankValidation.NOT_APPLICABLE);
|
||||||
|
diagnostics = List.copyOf(Objects.requireNonNullElse(diagnostics, List.of()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,279 @@
|
|||||||
|
package p.studio.workspaces.assets.scene;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import p.packer.dtos.PackerDiagnosticDTO;
|
||||||
|
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
|
||||||
|
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
|
||||||
|
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
|
||||||
|
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
||||||
|
import p.studio.workspaces.assets.messages.AssetWorkspaceSceneBankStatus;
|
||||||
|
import p.studio.workspaces.assets.messages.AssetWorkspaceSceneBankValidation;
|
||||||
|
import p.studio.workspaces.assets.tiled.TiledMapDocument;
|
||||||
|
import p.studio.workspaces.assets.tiled.TiledTilesetDocument;
|
||||||
|
import p.studio.workspaces.assets.tiled.TiledUnsupportedFeatureException;
|
||||||
|
import p.studio.workspaces.assets.tiled.TiledXmlCodec;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class SceneBankWorkflowService {
|
||||||
|
private static final String VALIDATION_FILE = "scene-bank.validation.json";
|
||||||
|
private static final String ACCEPTANCE_FILE = "scene-bank.acceptance.json";
|
||||||
|
|
||||||
|
private final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
private final TiledXmlCodec codec = new TiledXmlCodec();
|
||||||
|
|
||||||
|
public SceneBankWorkflowResult inspect(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||||
|
if (details == null || details.summary().assetFamily() != p.packer.messages.assets.AssetFamilyCatalog.SCENE_BANK) {
|
||||||
|
return new SceneBankWorkflowResult(true, "", AssetWorkspaceSceneBankValidation.NOT_APPLICABLE, List.of());
|
||||||
|
}
|
||||||
|
final List<PackerDiagnosticDTO> diagnostics = validateDiagnostics(projectReference, details);
|
||||||
|
final String currentFingerprint = diagnostics.stream().anyMatch(PackerDiagnosticDTO::blocking)
|
||||||
|
? ""
|
||||||
|
: computeFingerprint(projectReference, details);
|
||||||
|
final String validatedFingerprint = readFingerprint(details.summary().assetRoot().resolve(VALIDATION_FILE));
|
||||||
|
final String acceptedFingerprint = readFingerprint(details.summary().assetRoot().resolve(ACCEPTANCE_FILE));
|
||||||
|
final AssetWorkspaceSceneBankValidation validation = validationState(
|
||||||
|
diagnostics,
|
||||||
|
currentFingerprint,
|
||||||
|
validatedFingerprint,
|
||||||
|
acceptedFingerprint);
|
||||||
|
return new SceneBankWorkflowResult(
|
||||||
|
diagnostics.stream().noneMatch(PackerDiagnosticDTO::blocking),
|
||||||
|
messageFor(validation),
|
||||||
|
validation,
|
||||||
|
diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SceneBankWorkflowResult validate(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||||
|
final SceneBankWorkflowResult inspection = inspect(projectReference, details);
|
||||||
|
final Path validationFile = details.summary().assetRoot().resolve(VALIDATION_FILE);
|
||||||
|
try {
|
||||||
|
if (inspection.success() && !inspection.validation().currentFingerprint().isBlank()) {
|
||||||
|
writeFingerprint(validationFile, inspection.validation().currentFingerprint());
|
||||||
|
} else {
|
||||||
|
Files.deleteIfExists(validationFile);
|
||||||
|
}
|
||||||
|
} catch (IOException exception) {
|
||||||
|
return new SceneBankWorkflowResult(
|
||||||
|
false,
|
||||||
|
"Unable to persist validation state: " + exception.getMessage(),
|
||||||
|
inspection.validation(),
|
||||||
|
inspection.diagnostics());
|
||||||
|
}
|
||||||
|
return inspect(projectReference, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SceneBankWorkflowResult accept(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||||
|
final SceneBankWorkflowResult inspection = inspect(projectReference, details);
|
||||||
|
if (!inspection.validation().canAccept()) {
|
||||||
|
return new SceneBankWorkflowResult(
|
||||||
|
false,
|
||||||
|
"Scene Bank must validate successfully before acceptance.",
|
||||||
|
inspection.validation(),
|
||||||
|
inspection.diagnostics());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
writeFingerprint(details.summary().assetRoot().resolve(ACCEPTANCE_FILE), inspection.validation().currentFingerprint());
|
||||||
|
writeFingerprint(details.summary().assetRoot().resolve(VALIDATION_FILE), inspection.validation().currentFingerprint());
|
||||||
|
} catch (IOException exception) {
|
||||||
|
return new SceneBankWorkflowResult(
|
||||||
|
false,
|
||||||
|
"Unable to persist acceptance state: " + exception.getMessage(),
|
||||||
|
inspection.validation(),
|
||||||
|
inspection.diagnostics());
|
||||||
|
}
|
||||||
|
return inspect(projectReference, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PackerDiagnosticDTO> validateDiagnostics(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||||
|
final List<PackerDiagnosticDTO> diagnostics = new ArrayList<>();
|
||||||
|
final AssetStudioSceneBankMetadata metadata = details.sceneBankMetadata();
|
||||||
|
if (metadata == null) {
|
||||||
|
diagnostics.add(error("Scene Bank support metadata is missing or invalid.", details.summary().assetRoot().resolve("scene-bank.studio.json")));
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
|
||||||
|
if (binding.tilesetAssetRoot().isBlank()) {
|
||||||
|
diagnostics.add(error("Layer " + binding.index() + " is missing tileset_asset_root.", metadata.supportFile()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final Path tmxPath = details.summary().assetRoot().resolve(binding.tilemap()).toAbsolutePath().normalize();
|
||||||
|
final Path tsxPath = projectReference.rootPath()
|
||||||
|
.resolve("assets")
|
||||||
|
.resolve(binding.tilesetAssetRoot())
|
||||||
|
.resolve("tileset.tsx")
|
||||||
|
.toAbsolutePath()
|
||||||
|
.normalize();
|
||||||
|
if (!Files.isRegularFile(tmxPath)) {
|
||||||
|
diagnostics.add(error("TMX file is missing for layer " + binding.index() + ": " + binding.tilemap(), tmxPath));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!Files.isRegularFile(tsxPath)) {
|
||||||
|
diagnostics.add(error("Referenced TSX file is missing for layer " + binding.index() + ": " + binding.tilesetAssetRoot(), tsxPath));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final TiledTilesetDocument tileset = codec.readTileset(tsxPath);
|
||||||
|
final TiledMapDocument map = codec.readMap(tmxPath);
|
||||||
|
final String expectedSource = details.summary().assetRoot()
|
||||||
|
.toAbsolutePath()
|
||||||
|
.normalize()
|
||||||
|
.relativize(tsxPath)
|
||||||
|
.toString()
|
||||||
|
.replace('\\', '/');
|
||||||
|
if (map.tilesets().size() != 1) {
|
||||||
|
diagnostics.add(error("TMX must reference exactly one TSX in wave 1: " + binding.tilemap(), tmxPath));
|
||||||
|
} else if (!expectedSource.equals(map.tilesets().getFirst().source())) {
|
||||||
|
diagnostics.add(error("TMX tileset reference does not match support metadata: " + binding.tilemap(), tmxPath));
|
||||||
|
}
|
||||||
|
if (map.width() != metadata.mapWidth() || map.height() != metadata.mapHeight()) {
|
||||||
|
diagnostics.add(error("TMX dimensions do not match scene-bank support metadata: " + binding.tilemap(), tmxPath));
|
||||||
|
}
|
||||||
|
if (map.tileWidth() != tileset.tileWidth() || map.tileHeight() != tileset.tileHeight()) {
|
||||||
|
diagnostics.add(error("TMX tile size does not match referenced TSX: " + binding.tilemap(), tmxPath));
|
||||||
|
}
|
||||||
|
if (map.tileLayers().size() != 1 || !binding.layerName().equals(map.tileLayers().getFirst().name())) {
|
||||||
|
diagnostics.add(error("TMX layer mapping does not match scene-bank support metadata: " + binding.tilemap(), tmxPath));
|
||||||
|
}
|
||||||
|
} catch (TiledUnsupportedFeatureException exception) {
|
||||||
|
diagnostics.add(error(exception.getMessage(), tmxPath));
|
||||||
|
} catch (IOException exception) {
|
||||||
|
diagnostics.add(error("Unable to validate Scene Bank XML: " + exception.getMessage(), tmxPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String computeFingerprint(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||||
|
final AssetStudioSceneBankMetadata metadata = details.sceneBankMetadata();
|
||||||
|
if (metadata == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
final List<Path> files = new ArrayList<>();
|
||||||
|
files.add(metadata.supportFile());
|
||||||
|
for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
|
||||||
|
files.add(details.summary().assetRoot().resolve(binding.tilemap()).toAbsolutePath().normalize());
|
||||||
|
if (!binding.tilesetAssetRoot().isBlank()) {
|
||||||
|
files.add(projectReference.rootPath().resolve("assets").resolve(binding.tilesetAssetRoot()).resolve("tileset.tsx").toAbsolutePath().normalize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files.stream()
|
||||||
|
.distinct()
|
||||||
|
.sorted(Comparator.comparing(Path::toString))
|
||||||
|
.forEach(path -> updateDigest(digest, path));
|
||||||
|
return HexFormat.of().formatHex(digest.digest());
|
||||||
|
} catch (Exception exception) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDigest(MessageDigest digest, Path path) {
|
||||||
|
try {
|
||||||
|
if (!Files.isRegularFile(path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
digest.update(path.toString().getBytes(StandardCharsets.UTF_8));
|
||||||
|
digest.update((byte) 0);
|
||||||
|
digest.update(Files.readAllBytes(path));
|
||||||
|
digest.update((byte) 0);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetWorkspaceSceneBankValidation validationState(
|
||||||
|
List<PackerDiagnosticDTO> diagnostics,
|
||||||
|
String currentFingerprint,
|
||||||
|
String validatedFingerprint,
|
||||||
|
String acceptedFingerprint) {
|
||||||
|
if (diagnostics.stream().anyMatch(PackerDiagnosticDTO::blocking)) {
|
||||||
|
return new AssetWorkspaceSceneBankValidation(
|
||||||
|
AssetWorkspaceSceneBankStatus.VALIDATION_FAILED,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
currentFingerprint,
|
||||||
|
validatedFingerprint,
|
||||||
|
acceptedFingerprint);
|
||||||
|
}
|
||||||
|
final boolean validated = !currentFingerprint.isBlank() && currentFingerprint.equals(validatedFingerprint);
|
||||||
|
final boolean accepted = validated && currentFingerprint.equals(acceptedFingerprint);
|
||||||
|
if (accepted) {
|
||||||
|
return new AssetWorkspaceSceneBankValidation(
|
||||||
|
AssetWorkspaceSceneBankStatus.READY,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
currentFingerprint,
|
||||||
|
validatedFingerprint,
|
||||||
|
acceptedFingerprint);
|
||||||
|
}
|
||||||
|
if (validated) {
|
||||||
|
return new AssetWorkspaceSceneBankValidation(
|
||||||
|
AssetWorkspaceSceneBankStatus.VALIDATED_PENDING_ACCEPTANCE,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
currentFingerprint,
|
||||||
|
validatedFingerprint,
|
||||||
|
acceptedFingerprint);
|
||||||
|
}
|
||||||
|
return new AssetWorkspaceSceneBankValidation(
|
||||||
|
AssetWorkspaceSceneBankStatus.PENDING_VALIDATION,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
currentFingerprint,
|
||||||
|
validatedFingerprint,
|
||||||
|
acceptedFingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String messageFor(AssetWorkspaceSceneBankValidation validation) {
|
||||||
|
return switch (validation.status()) {
|
||||||
|
case NOT_APPLICABLE -> "";
|
||||||
|
case PENDING_VALIDATION -> "Scene Bank has pending external changes and requires validation.";
|
||||||
|
case VALIDATED_PENDING_ACCEPTANCE -> "Scene Bank validation succeeded and is waiting for explicit acceptance.";
|
||||||
|
case READY -> "Scene Bank is ready.";
|
||||||
|
case VALIDATION_FAILED -> "Scene Bank validation failed.";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readFingerprint(Path path) {
|
||||||
|
if (!Files.isRegularFile(path)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final JsonNode root = mapper.readTree(path.toFile());
|
||||||
|
return root.path("fingerprint").asText("").trim();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeFingerprint(Path path, String fingerprint) throws IOException {
|
||||||
|
final var root = mapper.createObjectNode();
|
||||||
|
root.put("schema_version", 1);
|
||||||
|
root.put("fingerprint", Objects.requireNonNullElse(fingerprint, ""));
|
||||||
|
root.put("recorded_at", Instant.now().toString());
|
||||||
|
mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PackerDiagnosticDTO error(String message, Path path) {
|
||||||
|
return new PackerDiagnosticDTO(
|
||||||
|
PackerDiagnosticSeverity.ERROR,
|
||||||
|
PackerDiagnosticCategory.STRUCTURAL,
|
||||||
|
message,
|
||||||
|
path,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledAssetGenerationResult(
|
||||||
|
boolean success,
|
||||||
|
String message,
|
||||||
|
List<Path> writtenFiles) {
|
||||||
|
|
||||||
|
public TiledAssetGenerationResult {
|
||||||
|
message = Objects.requireNonNullElse(message, "");
|
||||||
|
writtenFiles = List.copyOf(Objects.requireNonNullElse(writtenFiles, List.of()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,152 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import p.packer.dtos.PackerCodecConfigurationFieldDTO;
|
||||||
|
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
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.AssetWorkspaceAssetDetails;
|
||||||
|
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class TiledAssetGenerationService {
|
||||||
|
public static final String GENERATED_TSX_FILE = "tileset.tsx";
|
||||||
|
|
||||||
|
private final TiledXmlCodec codec = new TiledXmlCodec();
|
||||||
|
|
||||||
|
public TiledAssetGenerationResult generateTilesetTsx(AssetWorkspaceAssetDetails details) {
|
||||||
|
if (details == null
|
||||||
|
|| details.summary().assetFamily() != AssetFamilyCatalog.GLYPH_BANK
|
||||||
|
|| details.summary().glyphSpecialization() != AssetStudioGlyphSpecialization.TILESET) {
|
||||||
|
return new TiledAssetGenerationResult(false, "TSX generation is available only for Tileset-specialized glyph banks.", List.of());
|
||||||
|
}
|
||||||
|
final List<AssetWorkspaceBankCompositionFile> selectedFiles = details.bankComposition().selectedFiles();
|
||||||
|
if (selectedFiles.isEmpty()) {
|
||||||
|
return new TiledAssetGenerationResult(false, "Select at least one glyph artifact before generating TSX.", List.of());
|
||||||
|
}
|
||||||
|
final int tileSize = parseTileSize(details.metadataFields());
|
||||||
|
final List<TiledTilesetTile> tiles = new ArrayList<>();
|
||||||
|
for (int index = 0; index < selectedFiles.size(); index += 1) {
|
||||||
|
final AssetWorkspaceBankCompositionFile file = selectedFiles.get(index);
|
||||||
|
tiles.add(new TiledTilesetTile(
|
||||||
|
index,
|
||||||
|
file.path(),
|
||||||
|
tileSize,
|
||||||
|
tileSize,
|
||||||
|
List.of(new TiledProperty("glyph_id", "int", Integer.toString(index))),
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
final TiledTilesetDocument document = new TiledTilesetDocument(
|
||||||
|
"1.10",
|
||||||
|
"1.12.1",
|
||||||
|
details.summary().assetName(),
|
||||||
|
tileSize,
|
||||||
|
tileSize,
|
||||||
|
tiles.size(),
|
||||||
|
0,
|
||||||
|
List.of(),
|
||||||
|
tiles);
|
||||||
|
final Path outputPath = details.summary().assetRoot().resolve(GENERATED_TSX_FILE);
|
||||||
|
try {
|
||||||
|
codec.writeTileset(outputPath, document);
|
||||||
|
return new TiledAssetGenerationResult(true, "Generated TSX successfully.", List.of(outputPath));
|
||||||
|
} catch (IOException exception) {
|
||||||
|
return new TiledAssetGenerationResult(false, "Unable to generate TSX: " + exception.getMessage(), List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TiledAssetGenerationResult generateSceneBankTilemaps(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||||
|
if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.SCENE_BANK) {
|
||||||
|
return new TiledAssetGenerationResult(false, "TMX generation is available only for Scene Bank assets.", List.of());
|
||||||
|
}
|
||||||
|
final AssetStudioSceneBankMetadata metadata = details.sceneBankMetadata();
|
||||||
|
if (metadata == null) {
|
||||||
|
return new TiledAssetGenerationResult(false, "Scene Bank support metadata is missing or invalid.", List.of());
|
||||||
|
}
|
||||||
|
final List<Path> writtenFiles = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
|
||||||
|
final String tilesetAssetRoot = Objects.requireNonNullElse(binding.tilesetAssetRoot(), "").trim();
|
||||||
|
if (tilesetAssetRoot.isBlank()) {
|
||||||
|
return new TiledAssetGenerationResult(
|
||||||
|
false,
|
||||||
|
"Layer " + binding.index() + " must declare a non-blank tileset_asset_root before TMX generation.",
|
||||||
|
List.of());
|
||||||
|
}
|
||||||
|
final Path tilesetPath = projectReference.rootPath()
|
||||||
|
.resolve("assets")
|
||||||
|
.resolve(tilesetAssetRoot)
|
||||||
|
.resolve(GENERATED_TSX_FILE)
|
||||||
|
.toAbsolutePath()
|
||||||
|
.normalize();
|
||||||
|
if (!java.nio.file.Files.isRegularFile(tilesetPath)) {
|
||||||
|
return new TiledAssetGenerationResult(
|
||||||
|
false,
|
||||||
|
"Referenced TSX was not found for layer " + binding.index() + ": " + tilesetAssetRoot,
|
||||||
|
List.of());
|
||||||
|
}
|
||||||
|
final TiledTilesetDocument tileset = codec.readTileset(tilesetPath);
|
||||||
|
final String relativeTilesetPath = details.summary().assetRoot()
|
||||||
|
.toAbsolutePath()
|
||||||
|
.normalize()
|
||||||
|
.relativize(tilesetPath)
|
||||||
|
.toString()
|
||||||
|
.replace('\\', '/');
|
||||||
|
final int tileCount = metadata.mapWidth() * metadata.mapHeight();
|
||||||
|
final List<Long> gids = new ArrayList<>(tileCount);
|
||||||
|
for (int index = 0; index < tileCount; index += 1) {
|
||||||
|
gids.add(0L);
|
||||||
|
}
|
||||||
|
final TiledMapDocument document = new TiledMapDocument(
|
||||||
|
"1.10",
|
||||||
|
"1.12.1",
|
||||||
|
"orthogonal",
|
||||||
|
"right-down",
|
||||||
|
metadata.mapWidth(),
|
||||||
|
metadata.mapHeight(),
|
||||||
|
tileset.tileWidth(),
|
||||||
|
tileset.tileHeight(),
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
List.of(),
|
||||||
|
List.of(new TiledTilesetReference(1, relativeTilesetPath)),
|
||||||
|
List.of(new TiledTileLayer(
|
||||||
|
1,
|
||||||
|
binding.layerName(),
|
||||||
|
metadata.mapWidth(),
|
||||||
|
metadata.mapHeight(),
|
||||||
|
gids,
|
||||||
|
List.of())),
|
||||||
|
List.of());
|
||||||
|
final Path outputPath = details.summary().assetRoot().resolve(binding.tilemap());
|
||||||
|
codec.writeMap(outputPath, document);
|
||||||
|
writtenFiles.add(outputPath);
|
||||||
|
}
|
||||||
|
return new TiledAssetGenerationResult(true, "Generated TMX successfully.", writtenFiles);
|
||||||
|
} catch (TiledUnsupportedFeatureException | IOException exception) {
|
||||||
|
return new TiledAssetGenerationResult(false, "Unable to generate TMX: " + exception.getMessage(), List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseTileSize(List<PackerCodecConfigurationFieldDTO> metadataFields) {
|
||||||
|
final String value = metadataFields.stream()
|
||||||
|
.filter(field -> "tile_size".equals(field.key()))
|
||||||
|
.map(PackerCodecConfigurationFieldDTO::value)
|
||||||
|
.findFirst()
|
||||||
|
.orElse("16x16");
|
||||||
|
final String normalized = value.trim().toLowerCase();
|
||||||
|
if (normalized.startsWith("8x8")) {
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
if (normalized.startsWith("32x32")) {
|
||||||
|
return 32;
|
||||||
|
}
|
||||||
|
return 16;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledMapDocument(
|
||||||
|
String version,
|
||||||
|
String tiledVersion,
|
||||||
|
String orientation,
|
||||||
|
String renderOrder,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int tileWidth,
|
||||||
|
int tileHeight,
|
||||||
|
int nextLayerId,
|
||||||
|
int nextObjectId,
|
||||||
|
List<TiledProperty> properties,
|
||||||
|
List<TiledTilesetReference> tilesets,
|
||||||
|
List<TiledTileLayer> tileLayers,
|
||||||
|
List<TiledObjectLayer> objectLayers) {
|
||||||
|
|
||||||
|
public TiledMapDocument {
|
||||||
|
version = Objects.requireNonNullElse(version, "1.10");
|
||||||
|
tiledVersion = Objects.requireNonNullElse(tiledVersion, "1.12.1");
|
||||||
|
orientation = Objects.requireNonNullElse(orientation, "orthogonal");
|
||||||
|
renderOrder = Objects.requireNonNullElse(renderOrder, "right-down");
|
||||||
|
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||||
|
tilesets = List.copyOf(Objects.requireNonNull(tilesets, "tilesets"));
|
||||||
|
tileLayers = List.copyOf(Objects.requireNonNull(tileLayers, "tileLayers"));
|
||||||
|
objectLayers = List.copyOf(Objects.requireNonNull(objectLayers, "objectLayers"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledObjectData(
|
||||||
|
int id,
|
||||||
|
String name,
|
||||||
|
double x,
|
||||||
|
double y,
|
||||||
|
double width,
|
||||||
|
double height,
|
||||||
|
List<TiledPoint> polygonPoints,
|
||||||
|
List<TiledProperty> properties) {
|
||||||
|
|
||||||
|
public TiledObjectData {
|
||||||
|
name = Objects.requireNonNullElse(name, "").trim();
|
||||||
|
polygonPoints = List.copyOf(Objects.requireNonNull(polygonPoints, "polygonPoints"));
|
||||||
|
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledObjectLayer(
|
||||||
|
int id,
|
||||||
|
String name,
|
||||||
|
List<TiledObjectData> objects,
|
||||||
|
List<TiledProperty> properties) {
|
||||||
|
|
||||||
|
public TiledObjectLayer {
|
||||||
|
name = Objects.requireNonNullElse(name, "").trim();
|
||||||
|
objects = List.copyOf(Objects.requireNonNull(objects, "objects"));
|
||||||
|
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
public record TiledPoint(double x, double y) {
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledProperty(String name, String type, String value) {
|
||||||
|
public TiledProperty {
|
||||||
|
name = Objects.requireNonNull(name, "name").trim();
|
||||||
|
type = Objects.requireNonNullElse(type, "string").trim();
|
||||||
|
value = Objects.requireNonNullElse(value, "").trim();
|
||||||
|
if (name.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("name must not be blank");
|
||||||
|
}
|
||||||
|
if (type.isBlank()) {
|
||||||
|
type = "string";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledTileLayer(
|
||||||
|
int id,
|
||||||
|
String name,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
List<Long> gids,
|
||||||
|
List<TiledProperty> properties) {
|
||||||
|
|
||||||
|
public TiledTileLayer {
|
||||||
|
name = Objects.requireNonNullElse(name, "").trim();
|
||||||
|
gids = List.copyOf(Objects.requireNonNull(gids, "gids"));
|
||||||
|
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledTilesetDocument(
|
||||||
|
String version,
|
||||||
|
String tiledVersion,
|
||||||
|
String name,
|
||||||
|
int tileWidth,
|
||||||
|
int tileHeight,
|
||||||
|
int tileCount,
|
||||||
|
int columns,
|
||||||
|
List<TiledProperty> properties,
|
||||||
|
List<TiledTilesetTile> tiles) {
|
||||||
|
|
||||||
|
public TiledTilesetDocument {
|
||||||
|
version = Objects.requireNonNullElse(version, "1.10");
|
||||||
|
tiledVersion = Objects.requireNonNullElse(tiledVersion, "1.12.1");
|
||||||
|
name = Objects.requireNonNull(name, "name").trim();
|
||||||
|
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||||
|
tiles = List.copyOf(Objects.requireNonNull(tiles, "tiles"));
|
||||||
|
if (name.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("name must not be blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledTilesetReference(int firstGid, String source) {
|
||||||
|
public TiledTilesetReference {
|
||||||
|
source = Objects.requireNonNull(source, "source").trim();
|
||||||
|
if (firstGid <= 0) {
|
||||||
|
throw new IllegalArgumentException("firstGid must be positive");
|
||||||
|
}
|
||||||
|
if (source.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("source must not be blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledTilesetTile(
|
||||||
|
int id,
|
||||||
|
String imageSource,
|
||||||
|
int imageWidth,
|
||||||
|
int imageHeight,
|
||||||
|
List<TiledProperty> properties,
|
||||||
|
TiledObjectLayer collisionLayer) {
|
||||||
|
|
||||||
|
public TiledTilesetTile {
|
||||||
|
imageSource = Objects.requireNonNull(imageSource, "imageSource").trim();
|
||||||
|
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||||
|
if (imageSource.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("imageSource must not be blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
public final class TiledUnsupportedFeatureException extends RuntimeException {
|
||||||
|
public TiledUnsupportedFeatureException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,460 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
import javax.xml.transform.OutputKeys;
|
||||||
|
import javax.xml.transform.TransformerFactory;
|
||||||
|
import javax.xml.transform.dom.DOMSource;
|
||||||
|
import javax.xml.transform.stream.StreamResult;
|
||||||
|
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.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class TiledXmlCodec {
|
||||||
|
public TiledMapDocument readMap(Path path) throws IOException {
|
||||||
|
final Document document = parseDocument(path);
|
||||||
|
final Element root = document.getDocumentElement();
|
||||||
|
if (!"map".equals(root.getTagName())) {
|
||||||
|
throw new IOException("TMX root element must be <map>.");
|
||||||
|
}
|
||||||
|
if (!"0".equals(root.getAttribute("infinite")) && !root.getAttribute("infinite").isBlank()) {
|
||||||
|
throw new TiledUnsupportedFeatureException("Infinite maps are not supported in wave 1.");
|
||||||
|
}
|
||||||
|
rejectMapUnsupportedChildren(root);
|
||||||
|
return new TiledMapDocument(
|
||||||
|
root.getAttribute("version"),
|
||||||
|
root.getAttribute("tiledversion"),
|
||||||
|
root.getAttribute("orientation"),
|
||||||
|
root.getAttribute("renderorder"),
|
||||||
|
intAttribute(root, "width", 0),
|
||||||
|
intAttribute(root, "height", 0),
|
||||||
|
intAttribute(root, "tilewidth", 0),
|
||||||
|
intAttribute(root, "tileheight", 0),
|
||||||
|
intAttribute(root, "nextlayerid", 0),
|
||||||
|
intAttribute(root, "nextobjectid", 0),
|
||||||
|
readProperties(child(root, "properties")),
|
||||||
|
readTilesets(root),
|
||||||
|
readTileLayers(root),
|
||||||
|
readObjectLayers(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeMap(Path path, TiledMapDocument map) throws IOException {
|
||||||
|
final Document document = newDocument();
|
||||||
|
final Element root = document.createElement("map");
|
||||||
|
document.appendChild(root);
|
||||||
|
root.setAttribute("version", map.version());
|
||||||
|
root.setAttribute("tiledversion", map.tiledVersion());
|
||||||
|
root.setAttribute("orientation", map.orientation());
|
||||||
|
root.setAttribute("renderorder", map.renderOrder());
|
||||||
|
root.setAttribute("width", Integer.toString(map.width()));
|
||||||
|
root.setAttribute("height", Integer.toString(map.height()));
|
||||||
|
root.setAttribute("tilewidth", Integer.toString(map.tileWidth()));
|
||||||
|
root.setAttribute("tileheight", Integer.toString(map.tileHeight()));
|
||||||
|
root.setAttribute("infinite", "0");
|
||||||
|
root.setAttribute("nextlayerid", Integer.toString(map.nextLayerId()));
|
||||||
|
root.setAttribute("nextobjectid", Integer.toString(map.nextObjectId()));
|
||||||
|
appendProperties(document, root, map.properties());
|
||||||
|
for (TiledTilesetReference tileset : map.tilesets()) {
|
||||||
|
final Element tilesetElement = document.createElement("tileset");
|
||||||
|
tilesetElement.setAttribute("firstgid", Integer.toString(tileset.firstGid()));
|
||||||
|
tilesetElement.setAttribute("source", tileset.source());
|
||||||
|
root.appendChild(tilesetElement);
|
||||||
|
}
|
||||||
|
for (TiledTileLayer layer : map.tileLayers()) {
|
||||||
|
final Element layerElement = document.createElement("layer");
|
||||||
|
layerElement.setAttribute("id", Integer.toString(layer.id()));
|
||||||
|
layerElement.setAttribute("name", layer.name());
|
||||||
|
layerElement.setAttribute("width", Integer.toString(layer.width()));
|
||||||
|
layerElement.setAttribute("height", Integer.toString(layer.height()));
|
||||||
|
appendProperties(document, layerElement, layer.properties());
|
||||||
|
final Element dataElement = document.createElement("data");
|
||||||
|
dataElement.setAttribute("encoding", "csv");
|
||||||
|
dataElement.setTextContent(csv(layer.gids(), layer.width()));
|
||||||
|
layerElement.appendChild(dataElement);
|
||||||
|
root.appendChild(layerElement);
|
||||||
|
}
|
||||||
|
for (TiledObjectLayer layer : map.objectLayers()) {
|
||||||
|
root.appendChild(writeObjectLayer(document, layer, false));
|
||||||
|
}
|
||||||
|
writeDocument(path, document);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TiledTilesetDocument readTileset(Path path) throws IOException {
|
||||||
|
final Document document = parseDocument(path);
|
||||||
|
final Element root = document.getDocumentElement();
|
||||||
|
if (!"tileset".equals(root.getTagName())) {
|
||||||
|
throw new IOException("TSX root element must be <tileset>.");
|
||||||
|
}
|
||||||
|
rejectTilesetUnsupportedChildren(root);
|
||||||
|
final List<TiledTilesetTile> tiles = new ArrayList<>();
|
||||||
|
for (Element tileElement : children(root, "tile")) {
|
||||||
|
if (child(tileElement, "animation") != null) {
|
||||||
|
throw new TiledUnsupportedFeatureException("Tiled animations are not supported in wave 1.");
|
||||||
|
}
|
||||||
|
final Element imageElement = child(tileElement, "image");
|
||||||
|
if (imageElement == null) {
|
||||||
|
throw new IOException("TSX tile must contain an <image> element.");
|
||||||
|
}
|
||||||
|
tiles.add(new TiledTilesetTile(
|
||||||
|
intAttribute(tileElement, "id", 0),
|
||||||
|
imageElement.getAttribute("source"),
|
||||||
|
intAttribute(imageElement, "width", 0),
|
||||||
|
intAttribute(imageElement, "height", 0),
|
||||||
|
readProperties(child(tileElement, "properties")),
|
||||||
|
readEmbeddedObjectLayer(tileElement)));
|
||||||
|
}
|
||||||
|
tiles.sort(Comparator.comparingInt(TiledTilesetTile::id));
|
||||||
|
return new TiledTilesetDocument(
|
||||||
|
root.getAttribute("version"),
|
||||||
|
root.getAttribute("tiledversion"),
|
||||||
|
root.getAttribute("name"),
|
||||||
|
intAttribute(root, "tilewidth", 0),
|
||||||
|
intAttribute(root, "tileheight", 0),
|
||||||
|
intAttribute(root, "tilecount", tiles.size()),
|
||||||
|
intAttribute(root, "columns", 0),
|
||||||
|
readProperties(child(root, "properties")),
|
||||||
|
tiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeTileset(Path path, TiledTilesetDocument tileset) throws IOException {
|
||||||
|
final Document document = newDocument();
|
||||||
|
final Element root = document.createElement("tileset");
|
||||||
|
document.appendChild(root);
|
||||||
|
root.setAttribute("version", tileset.version());
|
||||||
|
root.setAttribute("tiledversion", tileset.tiledVersion());
|
||||||
|
root.setAttribute("name", tileset.name());
|
||||||
|
root.setAttribute("tilewidth", Integer.toString(tileset.tileWidth()));
|
||||||
|
root.setAttribute("tileheight", Integer.toString(tileset.tileHeight()));
|
||||||
|
root.setAttribute("tilecount", Integer.toString(tileset.tileCount()));
|
||||||
|
root.setAttribute("columns", Integer.toString(tileset.columns()));
|
||||||
|
final Element grid = document.createElement("grid");
|
||||||
|
grid.setAttribute("orientation", "orthogonal");
|
||||||
|
grid.setAttribute("width", "1");
|
||||||
|
grid.setAttribute("height", "1");
|
||||||
|
root.appendChild(grid);
|
||||||
|
appendProperties(document, root, tileset.properties());
|
||||||
|
for (TiledTilesetTile tile : tileset.tiles()) {
|
||||||
|
final Element tileElement = document.createElement("tile");
|
||||||
|
tileElement.setAttribute("id", Integer.toString(tile.id()));
|
||||||
|
appendProperties(document, tileElement, tile.properties());
|
||||||
|
final Element imageElement = document.createElement("image");
|
||||||
|
imageElement.setAttribute("source", tile.imageSource());
|
||||||
|
imageElement.setAttribute("width", Integer.toString(tile.imageWidth()));
|
||||||
|
imageElement.setAttribute("height", Integer.toString(tile.imageHeight()));
|
||||||
|
tileElement.appendChild(imageElement);
|
||||||
|
if (tile.collisionLayer() != null) {
|
||||||
|
tileElement.appendChild(writeObjectLayer(document, tile.collisionLayer(), true));
|
||||||
|
}
|
||||||
|
root.appendChild(tileElement);
|
||||||
|
}
|
||||||
|
writeDocument(path, document);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Document parseDocument(Path path) throws IOException {
|
||||||
|
try {
|
||||||
|
final var factory = DocumentBuilderFactory.newInstance();
|
||||||
|
factory.setNamespaceAware(false);
|
||||||
|
return factory.newDocumentBuilder().parse(path.toFile());
|
||||||
|
} catch (TiledUnsupportedFeatureException exception) {
|
||||||
|
throw exception;
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new IOException("Unable to parse Tiled XML: " + exception.getMessage(), exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Document newDocument() throws IOException {
|
||||||
|
try {
|
||||||
|
return DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new IOException("Unable to create XML document: " + exception.getMessage(), exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeDocument(Path path, Document document) throws IOException {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(Objects.requireNonNull(path, "path").toAbsolutePath().normalize().getParent());
|
||||||
|
final var transformer = TransformerFactory.newInstance().newTransformer();
|
||||||
|
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
|
||||||
|
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
|
||||||
|
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
|
||||||
|
transformer.transform(new DOMSource(document), new StreamResult(path.toFile()));
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new IOException("Unable to write Tiled XML: " + exception.getMessage(), exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rejectMapUnsupportedChildren(Element root) {
|
||||||
|
for (Element child : childElements(root)) {
|
||||||
|
final String tag = child.getTagName();
|
||||||
|
if ("imagelayer".equals(tag) || "group".equals(tag) || "template".equals(tag)) {
|
||||||
|
throw new TiledUnsupportedFeatureException("Unsupported TMX feature in wave 1: " + tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rejectTilesetUnsupportedChildren(Element root) {
|
||||||
|
for (Element child : childElements(root)) {
|
||||||
|
final String tag = child.getTagName();
|
||||||
|
if ("wangsets".equals(tag) || "tileoffset".equals(tag) || "transformations".equals(tag)) {
|
||||||
|
throw new TiledUnsupportedFeatureException("Unsupported TSX feature in wave 1: " + tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TiledTilesetReference> readTilesets(Element root) {
|
||||||
|
final List<TiledTilesetReference> tilesets = new ArrayList<>();
|
||||||
|
for (Element tilesetElement : children(root, "tileset")) {
|
||||||
|
if (!tilesetElement.hasAttribute("source")) {
|
||||||
|
throw new TiledUnsupportedFeatureException("Inline tilesets are not supported in wave 1.");
|
||||||
|
}
|
||||||
|
tilesets.add(new TiledTilesetReference(
|
||||||
|
intAttribute(tilesetElement, "firstgid", 1),
|
||||||
|
tilesetElement.getAttribute("source")));
|
||||||
|
}
|
||||||
|
return List.copyOf(tilesets);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TiledTileLayer> readTileLayers(Element root) {
|
||||||
|
final List<TiledTileLayer> layers = new ArrayList<>();
|
||||||
|
for (Element layerElement : children(root, "layer")) {
|
||||||
|
final Element dataElement = child(layerElement, "data");
|
||||||
|
if (dataElement == null || !"csv".equalsIgnoreCase(dataElement.getAttribute("encoding"))) {
|
||||||
|
throw new TiledUnsupportedFeatureException("Wave 1 supports only CSV tile layer encoding.");
|
||||||
|
}
|
||||||
|
layers.add(new TiledTileLayer(
|
||||||
|
intAttribute(layerElement, "id", 0),
|
||||||
|
layerElement.getAttribute("name"),
|
||||||
|
intAttribute(layerElement, "width", 0),
|
||||||
|
intAttribute(layerElement, "height", 0),
|
||||||
|
parseCsvData(dataElement.getTextContent()),
|
||||||
|
readProperties(child(layerElement, "properties"))));
|
||||||
|
}
|
||||||
|
return List.copyOf(layers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TiledObjectLayer> readObjectLayers(Element root) {
|
||||||
|
final List<TiledObjectLayer> layers = new ArrayList<>();
|
||||||
|
for (Element objectGroup : children(root, "objectgroup")) {
|
||||||
|
layers.add(readObjectLayer(objectGroup));
|
||||||
|
}
|
||||||
|
return List.copyOf(layers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TiledObjectLayer readEmbeddedObjectLayer(Element parent) {
|
||||||
|
final Element objectGroup = child(parent, "objectgroup");
|
||||||
|
return objectGroup == null ? null : readObjectLayer(objectGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TiledObjectLayer readObjectLayer(Element objectGroup) {
|
||||||
|
final List<TiledObjectData> objects = new ArrayList<>();
|
||||||
|
for (Element objectElement : children(objectGroup, "object")) {
|
||||||
|
final Element polygon = child(objectElement, "polygon");
|
||||||
|
if (child(objectElement, "ellipse") != null || child(objectElement, "point") != null || child(objectElement, "polyline") != null) {
|
||||||
|
throw new TiledUnsupportedFeatureException("Unsupported object-layer geometry in wave 1.");
|
||||||
|
}
|
||||||
|
objects.add(new TiledObjectData(
|
||||||
|
intAttribute(objectElement, "id", 0),
|
||||||
|
objectElement.getAttribute("name"),
|
||||||
|
doubleAttribute(objectElement, "x", 0.0d),
|
||||||
|
doubleAttribute(objectElement, "y", 0.0d),
|
||||||
|
doubleAttribute(objectElement, "width", 0.0d),
|
||||||
|
doubleAttribute(objectElement, "height", 0.0d),
|
||||||
|
polygon == null ? List.of() : parsePolygon(polygon.getAttribute("points")),
|
||||||
|
readProperties(child(objectElement, "properties"))));
|
||||||
|
}
|
||||||
|
return new TiledObjectLayer(
|
||||||
|
intAttribute(objectGroup, "id", 0),
|
||||||
|
objectGroup.getAttribute("name"),
|
||||||
|
objects,
|
||||||
|
readProperties(child(objectGroup, "properties")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Element writeObjectLayer(Document document, TiledObjectLayer layer, boolean embedded) {
|
||||||
|
final Element objectGroup = document.createElement("objectgroup");
|
||||||
|
if (layer.id() > 0) {
|
||||||
|
objectGroup.setAttribute("id", Integer.toString(layer.id()));
|
||||||
|
}
|
||||||
|
if (!layer.name().isBlank()) {
|
||||||
|
objectGroup.setAttribute("name", layer.name());
|
||||||
|
}
|
||||||
|
if (embedded) {
|
||||||
|
objectGroup.setAttribute("draworder", "index");
|
||||||
|
}
|
||||||
|
appendProperties(document, objectGroup, layer.properties());
|
||||||
|
for (TiledObjectData object : layer.objects()) {
|
||||||
|
final Element objectElement = document.createElement("object");
|
||||||
|
if (object.id() > 0) {
|
||||||
|
objectElement.setAttribute("id", Integer.toString(object.id()));
|
||||||
|
}
|
||||||
|
if (!object.name().isBlank()) {
|
||||||
|
objectElement.setAttribute("name", object.name());
|
||||||
|
}
|
||||||
|
objectElement.setAttribute("x", formatDecimal(object.x()));
|
||||||
|
objectElement.setAttribute("y", formatDecimal(object.y()));
|
||||||
|
if (object.width() > 0.0d) {
|
||||||
|
objectElement.setAttribute("width", formatDecimal(object.width()));
|
||||||
|
}
|
||||||
|
if (object.height() > 0.0d) {
|
||||||
|
objectElement.setAttribute("height", formatDecimal(object.height()));
|
||||||
|
}
|
||||||
|
appendProperties(document, objectElement, object.properties());
|
||||||
|
if (!object.polygonPoints().isEmpty()) {
|
||||||
|
final Element polygon = document.createElement("polygon");
|
||||||
|
polygon.setAttribute("points", polygonPoints(object.polygonPoints()));
|
||||||
|
objectElement.appendChild(polygon);
|
||||||
|
}
|
||||||
|
objectGroup.appendChild(objectElement);
|
||||||
|
}
|
||||||
|
return objectGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendProperties(Document document, Element parent, List<TiledProperty> properties) {
|
||||||
|
if (properties.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Element propertiesElement = document.createElement("properties");
|
||||||
|
for (TiledProperty property : properties) {
|
||||||
|
final Element propertyElement = document.createElement("property");
|
||||||
|
propertyElement.setAttribute("name", property.name());
|
||||||
|
if (!"string".equals(property.type())) {
|
||||||
|
propertyElement.setAttribute("type", property.type());
|
||||||
|
}
|
||||||
|
propertyElement.setAttribute("value", property.value());
|
||||||
|
propertiesElement.appendChild(propertyElement);
|
||||||
|
}
|
||||||
|
parent.appendChild(propertiesElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TiledProperty> readProperties(Element propertiesElement) {
|
||||||
|
if (propertiesElement == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
final List<TiledProperty> properties = new ArrayList<>();
|
||||||
|
for (Element propertyElement : children(propertiesElement, "property")) {
|
||||||
|
properties.add(new TiledProperty(
|
||||||
|
propertyElement.getAttribute("name"),
|
||||||
|
propertyElement.getAttribute("type"),
|
||||||
|
propertyElement.hasAttribute("value")
|
||||||
|
? propertyElement.getAttribute("value")
|
||||||
|
: propertyElement.getTextContent()));
|
||||||
|
}
|
||||||
|
return List.copyOf(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Long> parseCsvData(String text) {
|
||||||
|
final List<Long> gids = new ArrayList<>();
|
||||||
|
for (String token : Objects.requireNonNullElse(text, "").split(",")) {
|
||||||
|
final String normalized = token.trim();
|
||||||
|
if (normalized.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
gids.add(Long.parseLong(normalized));
|
||||||
|
}
|
||||||
|
return List.copyOf(gids);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TiledPoint> parsePolygon(String points) {
|
||||||
|
final List<TiledPoint> parsed = new ArrayList<>();
|
||||||
|
for (String segment : Objects.requireNonNullElse(points, "").trim().split(" ")) {
|
||||||
|
final String normalized = segment.trim();
|
||||||
|
if (normalized.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final String[] pair = normalized.split(",");
|
||||||
|
if (pair.length != 2) {
|
||||||
|
throw new TiledUnsupportedFeatureException("Invalid polygon point format in Tiled XML.");
|
||||||
|
}
|
||||||
|
parsed.add(new TiledPoint(Double.parseDouble(pair[0]), Double.parseDouble(pair[1])));
|
||||||
|
}
|
||||||
|
return List.copyOf(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String csv(List<Long> gids, int width) {
|
||||||
|
final StringBuilder builder = new StringBuilder();
|
||||||
|
for (int index = 0; index < gids.size(); index += 1) {
|
||||||
|
if (index > 0) {
|
||||||
|
builder.append(index % Math.max(width, 1) == 0 ? ",\n" : ",");
|
||||||
|
}
|
||||||
|
builder.append(Long.toUnsignedString(gids.get(index)));
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String polygonPoints(List<TiledPoint> points) {
|
||||||
|
final StringBuilder builder = new StringBuilder();
|
||||||
|
for (int index = 0; index < points.size(); index += 1) {
|
||||||
|
if (index > 0) {
|
||||||
|
builder.append(' ');
|
||||||
|
}
|
||||||
|
builder.append(formatDecimal(points.get(index).x()))
|
||||||
|
.append(',')
|
||||||
|
.append(formatDecimal(points.get(index).y()));
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int intAttribute(Element element, String attribute, int fallback) {
|
||||||
|
final String value = element.getAttribute(attribute);
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return Integer.parseInt(value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private double doubleAttribute(Element element, String attribute, double fallback) {
|
||||||
|
final String value = element.getAttribute(attribute);
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return Double.parseDouble(value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatDecimal(double value) {
|
||||||
|
if (Math.rint(value) == value) {
|
||||||
|
return Long.toString(Math.round(value));
|
||||||
|
}
|
||||||
|
return String.format(Locale.ROOT, "%.6f", value)
|
||||||
|
.replaceAll("0+$", "")
|
||||||
|
.replaceAll("\\.$", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Element child(Element parent, String name) {
|
||||||
|
for (Element child : childElements(parent)) {
|
||||||
|
if (name.equals(child.getTagName())) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Element> children(Element parent, String name) {
|
||||||
|
final List<Element> elements = new ArrayList<>();
|
||||||
|
for (Element child : childElements(parent)) {
|
||||||
|
if (name.equals(child.getTagName())) {
|
||||||
|
elements.add(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Element> childElements(Element parent) {
|
||||||
|
final NodeList children = parent.getChildNodes();
|
||||||
|
final List<Element> elements = new ArrayList<>();
|
||||||
|
for (int index = 0; index < children.getLength(); index += 1) {
|
||||||
|
final Node node = children.item(index);
|
||||||
|
if (node instanceof Element element) {
|
||||||
|
elements.add(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()) {
|
||||||
|
|||||||
@ -144,6 +144,10 @@ assets.section.actions=Actions
|
|||||||
assets.actions.empty=No actions available for this asset.
|
assets.actions.empty=No actions available for this asset.
|
||||||
assets.action.register=Register
|
assets.action.register=Register
|
||||||
assets.action.analyse=Analyse
|
assets.action.analyse=Analyse
|
||||||
|
assets.action.generateTsx=Generate TSX
|
||||||
|
assets.action.generateTmx=Generate TMX
|
||||||
|
assets.action.validateSceneBank=Validate Scene Bank
|
||||||
|
assets.action.acceptSceneBank=Accept Scene Bank
|
||||||
assets.action.delete=Delete
|
assets.action.delete=Delete
|
||||||
assets.deleteDialog.title=Delete Asset
|
assets.deleteDialog.title=Delete Asset
|
||||||
assets.deleteDialog.description=Type the asset name below to confirm deletion of {0}.
|
assets.deleteDialog.description=Type the asset name below to confirm deletion of {0}.
|
||||||
@ -181,10 +185,24 @@ 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.sceneStatus=Scene Status
|
||||||
|
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.sceneStatus.pendingValidation=Pending Validation
|
||||||
|
assets.sceneStatus.validatedPendingAcceptance=Validated / Pending Acceptance
|
||||||
|
assets.sceneStatus.ready=Ready
|
||||||
|
assets.sceneStatus.validationFailed=Validation Failed
|
||||||
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 +263,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}
|
||||||
|
|||||||
@ -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,8 @@ final class AssetDetailsBankCompositionCoordinatorTest {
|
|||||||
List.of(),
|
List.of(),
|
||||||
0L),
|
0L),
|
||||||
List.of(),
|
List.of(),
|
||||||
|
null,
|
||||||
|
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||||
List.of());
|
List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,6 +139,8 @@ final class AssetDetailsBankCompositionCoordinatorTest {
|
|||||||
List.of(),
|
List.of(),
|
||||||
0L),
|
0L),
|
||||||
List.of(),
|
List.of(),
|
||||||
|
null,
|
||||||
|
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||||
List.of());
|
List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,6 +164,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);
|
||||||
|
|||||||
@ -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,8 @@ 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,
|
||||||
|
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||||
List.of());
|
List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,82 @@
|
|||||||
|
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,
|
||||||
|
"map_width": 20,
|
||||||
|
"map_height": 12,
|
||||||
|
"layer_count": 2,
|
||||||
|
"layers": [
|
||||||
|
{ "index": 1, "name": "Ground", "tilemap": "ground.tmx", "tileset_asset_root": "tilesets/ground" },
|
||||||
|
{ "index": 2, "name": "Collision", "tilemap": "collision.tmx", "tileset_asset_root": "tilesets/ground" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
final AssetStudioMetadataSnapshot snapshot = service.read(assetRoot, AssetFamilyCatalog.SCENE_BANK);
|
||||||
|
|
||||||
|
assertNotNull(snapshot.sceneBankMetadata());
|
||||||
|
assertEquals(20, snapshot.sceneBankMetadata().mapWidth());
|
||||||
|
assertEquals(12, snapshot.sceneBankMetadata().mapHeight());
|
||||||
|
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,
|
||||||
|
"map_width": 16,
|
||||||
|
"map_height": 16,
|
||||||
|
"layer_count": 5,
|
||||||
|
"layers": [
|
||||||
|
{ "index": 1, "name": "A", "tilemap": "a.tmx", "tileset_asset_root": "" },
|
||||||
|
{ "index": 2, "name": "B", "tilemap": "b.tmx", "tileset_asset_root": "" },
|
||||||
|
{ "index": 3, "name": "C", "tilemap": "c.tmx", "tileset_asset_root": "" },
|
||||||
|
{ "index": 4, "name": "D", "tilemap": "d.tmx", "tileset_asset_root": "" },
|
||||||
|
{ "index": 5, "name": "E", "tilemap": "e.tmx", "tileset_asset_root": "" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
final AssetStudioMetadataSnapshot snapshot = service.read(assetRoot, AssetFamilyCatalog.SCENE_BANK);
|
||||||
|
|
||||||
|
assertNull(snapshot.sceneBankMetadata());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,193 @@
|
|||||||
|
package p.studio.workspaces.assets.scene;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import p.packer.dtos.PackerCodecConfigurationFieldDTO;
|
||||||
|
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.packer.messages.assets.PackerCodecConfigurationFieldType;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
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.*;
|
||||||
|
import p.studio.workspaces.assets.tiled.TiledAssetGenerationService;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
final class SceneBankWorkflowServiceTest {
|
||||||
|
private final TiledAssetGenerationService generationService = new TiledAssetGenerationService();
|
||||||
|
private final SceneBankWorkflowService workflowService = new SceneBankWorkflowService();
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validateThenAcceptPromotesSceneBankToReadyUntilFilesChange() throws Exception {
|
||||||
|
final ProjectReference project = projectReference();
|
||||||
|
final AssetWorkspaceAssetDetails sceneDetails = createValidScene(project);
|
||||||
|
|
||||||
|
final SceneBankWorkflowResult initial = workflowService.inspect(project, sceneDetails);
|
||||||
|
assertEquals(AssetWorkspaceSceneBankStatus.PENDING_VALIDATION, initial.validation().status());
|
||||||
|
assertFalse(initial.validation().canAccept());
|
||||||
|
|
||||||
|
final SceneBankWorkflowResult validated = workflowService.validate(project, sceneDetails);
|
||||||
|
assertEquals(AssetWorkspaceSceneBankStatus.VALIDATED_PENDING_ACCEPTANCE, validated.validation().status());
|
||||||
|
assertTrue(validated.validation().canAccept());
|
||||||
|
|
||||||
|
final SceneBankWorkflowResult accepted = workflowService.accept(project, sceneDetails);
|
||||||
|
assertEquals(AssetWorkspaceSceneBankStatus.READY, accepted.validation().status());
|
||||||
|
assertFalse(accepted.validation().pendingExternalChanges());
|
||||||
|
|
||||||
|
final Path tmxPath = sceneDetails.summary().assetRoot().resolve("ground.tmx");
|
||||||
|
Files.writeString(tmxPath, Files.readString(tmxPath).replaceFirst("0,0,0", "1,0,0"));
|
||||||
|
|
||||||
|
final SceneBankWorkflowResult changed = workflowService.inspect(project, sceneDetails);
|
||||||
|
assertEquals(AssetWorkspaceSceneBankStatus.PENDING_VALIDATION, changed.validation().status());
|
||||||
|
assertTrue(changed.validation().pendingExternalChanges());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validationFailsWhenReferencedTsxIsMissing() throws Exception {
|
||||||
|
final ProjectReference project = projectReference();
|
||||||
|
final Path sceneRoot = project.rootPath().resolve("assets/scenes/broken");
|
||||||
|
Files.createDirectories(sceneRoot);
|
||||||
|
final AssetWorkspaceAssetDetails brokenScene = new AssetWorkspaceAssetDetails(
|
||||||
|
new AssetWorkspaceAssetSummary(
|
||||||
|
AssetReference.forAssetId(2),
|
||||||
|
"broken_scene",
|
||||||
|
AssetWorkspaceAssetState.REGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||||
|
2,
|
||||||
|
AssetFamilyCatalog.SCENE_BANK,
|
||||||
|
AssetStudioGlyphSpecialization.NONE,
|
||||||
|
sceneRoot,
|
||||||
|
false,
|
||||||
|
false),
|
||||||
|
List.of(),
|
||||||
|
OutputFormatCatalog.SCENE_TILED_V1,
|
||||||
|
OutputCodecCatalog.NONE,
|
||||||
|
List.of(OutputCodecCatalog.NONE),
|
||||||
|
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||||
|
List.of(),
|
||||||
|
Map.of(),
|
||||||
|
new AssetWorkspaceBankCompositionDetails(List.of(), List.of(), 0L),
|
||||||
|
List.of(),
|
||||||
|
new AssetStudioSceneBankMetadata(
|
||||||
|
8,
|
||||||
|
8,
|
||||||
|
1,
|
||||||
|
List.of(new AssetStudioSceneLayerBinding(1, "Ground", "ground.tmx", "tilesets/missing")),
|
||||||
|
sceneRoot.resolve("scene-bank.studio.json")),
|
||||||
|
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||||
|
List.of());
|
||||||
|
|
||||||
|
Files.writeString(sceneRoot.resolve("ground.tmx"), """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down"
|
||||||
|
width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
|
||||||
|
<tileset firstgid="1" source="../../tilesets/missing/tileset.tsx"/>
|
||||||
|
<layer id="1" name="Ground" width="8" height="8">
|
||||||
|
<data encoding="csv">0</data>
|
||||||
|
</layer>
|
||||||
|
</map>
|
||||||
|
""");
|
||||||
|
|
||||||
|
final SceneBankWorkflowResult result = workflowService.inspect(project, brokenScene);
|
||||||
|
|
||||||
|
assertEquals(AssetWorkspaceSceneBankStatus.VALIDATION_FAILED, result.validation().status());
|
||||||
|
assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.message().contains("Referenced TSX file is missing")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProjectReference projectReference() {
|
||||||
|
return new ProjectReference("main", "1", "pbs", 1, tempDir.resolve("project"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetWorkspaceAssetDetails createValidScene(ProjectReference project) throws Exception {
|
||||||
|
final Path tilesetRoot = project.rootPath().resolve("assets/tilesets/overworld");
|
||||||
|
Files.createDirectories(tilesetRoot);
|
||||||
|
Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
|
||||||
|
assertTrue(generationService.generateTilesetTsx(tilesetDetails(tilesetRoot)).success());
|
||||||
|
|
||||||
|
final Path sceneRoot = project.rootPath().resolve("assets/scenes/overworld");
|
||||||
|
Files.createDirectories(sceneRoot);
|
||||||
|
final AssetWorkspaceAssetDetails scene = sceneDetails(sceneRoot);
|
||||||
|
assertTrue(generationService.generateSceneBankTilemaps(project, scene).success());
|
||||||
|
return scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetWorkspaceAssetDetails tilesetDetails(Path assetRoot) {
|
||||||
|
return new AssetWorkspaceAssetDetails(
|
||||||
|
new AssetWorkspaceAssetSummary(
|
||||||
|
AssetReference.forAssetId(1),
|
||||||
|
"overworld_tileset",
|
||||||
|
AssetWorkspaceAssetState.REGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||||
|
1,
|
||||||
|
AssetFamilyCatalog.GLYPH_BANK,
|
||||||
|
AssetStudioGlyphSpecialization.TILESET,
|
||||||
|
assetRoot,
|
||||||
|
false,
|
||||||
|
false),
|
||||||
|
List.of(),
|
||||||
|
OutputFormatCatalog.GLYPH_INDEXED_V1,
|
||||||
|
OutputCodecCatalog.NONE,
|
||||||
|
List.of(OutputCodecCatalog.NONE),
|
||||||
|
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||||
|
List.of(new PackerCodecConfigurationFieldDTO(
|
||||||
|
"tile_size",
|
||||||
|
"Tile Size",
|
||||||
|
PackerCodecConfigurationFieldType.ENUM,
|
||||||
|
"16x16",
|
||||||
|
true,
|
||||||
|
List.of("8x8", "16x16", "32x32"))),
|
||||||
|
Map.of(),
|
||||||
|
new AssetWorkspaceBankCompositionDetails(
|
||||||
|
List.of(),
|
||||||
|
List.of(new AssetWorkspaceBankCompositionFile("a.png", "a.png", 1L, 1L, null, Map.of())),
|
||||||
|
0L),
|
||||||
|
List.of(),
|
||||||
|
null,
|
||||||
|
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||||
|
List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetWorkspaceAssetDetails sceneDetails(Path assetRoot) {
|
||||||
|
return new AssetWorkspaceAssetDetails(
|
||||||
|
new AssetWorkspaceAssetSummary(
|
||||||
|
AssetReference.forAssetId(2),
|
||||||
|
"overworld_scene",
|
||||||
|
AssetWorkspaceAssetState.REGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||||
|
2,
|
||||||
|
AssetFamilyCatalog.SCENE_BANK,
|
||||||
|
AssetStudioGlyphSpecialization.NONE,
|
||||||
|
assetRoot,
|
||||||
|
false,
|
||||||
|
false),
|
||||||
|
List.of(),
|
||||||
|
OutputFormatCatalog.SCENE_TILED_V1,
|
||||||
|
OutputCodecCatalog.NONE,
|
||||||
|
List.of(OutputCodecCatalog.NONE),
|
||||||
|
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||||
|
List.of(),
|
||||||
|
Map.of(),
|
||||||
|
new AssetWorkspaceBankCompositionDetails(List.of(), List.of(), 0L),
|
||||||
|
List.of(),
|
||||||
|
new AssetStudioSceneBankMetadata(
|
||||||
|
16,
|
||||||
|
12,
|
||||||
|
1,
|
||||||
|
List.of(new AssetStudioSceneLayerBinding(1, "Ground", "ground.tmx", "tilesets/overworld")),
|
||||||
|
assetRoot.resolve("scene-bank.studio.json")),
|
||||||
|
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||||
|
List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,143 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import p.packer.dtos.PackerCodecConfigurationFieldDTO;
|
||||||
|
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.packer.messages.assets.PackerCodecConfigurationFieldType;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
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.*;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
final class TiledAssetGenerationServiceTest {
|
||||||
|
private final TiledAssetGenerationService service = new TiledAssetGenerationService();
|
||||||
|
private final TiledXmlCodec codec = new TiledXmlCodec();
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void generatesTsxForTilesetSpecializedGlyphBank() throws Exception {
|
||||||
|
final Path assetRoot = tempDir.resolve("assets/tilesets/overworld");
|
||||||
|
Files.createDirectories(assetRoot);
|
||||||
|
Files.writeString(assetRoot.resolve("a.png"), "fixture");
|
||||||
|
Files.writeString(assetRoot.resolve("b.png"), "fixture");
|
||||||
|
|
||||||
|
final TiledAssetGenerationResult result = service.generateTilesetTsx(tilesetDetails(assetRoot));
|
||||||
|
|
||||||
|
assertTrue(result.success());
|
||||||
|
final Path tsxPath = assetRoot.resolve(TiledAssetGenerationService.GENERATED_TSX_FILE);
|
||||||
|
assertTrue(Files.isRegularFile(tsxPath));
|
||||||
|
final TiledTilesetDocument tileset = codec.readTileset(tsxPath);
|
||||||
|
assertEquals(2, tileset.tiles().size());
|
||||||
|
assertEquals("a.png", tileset.tiles().get(0).imageSource());
|
||||||
|
assertEquals("glyph_id", tileset.tiles().get(0).properties().getFirst().name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void generatesTmxFilesForSceneBankUsingReferencedTsx() throws Exception {
|
||||||
|
final Path projectRoot = tempDir.resolve("project");
|
||||||
|
final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
|
||||||
|
Files.createDirectories(tilesetRoot);
|
||||||
|
Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
|
||||||
|
assertTrue(service.generateTilesetTsx(tilesetDetails(tilesetRoot)).success());
|
||||||
|
|
||||||
|
final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
|
||||||
|
Files.createDirectories(sceneRoot);
|
||||||
|
final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
|
||||||
|
final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);
|
||||||
|
|
||||||
|
final TiledAssetGenerationResult result = service.generateSceneBankTilemaps(projectReference, details);
|
||||||
|
|
||||||
|
assertTrue(result.success());
|
||||||
|
final Path tmxPath = sceneRoot.resolve("ground.tmx");
|
||||||
|
assertTrue(Files.isRegularFile(tmxPath));
|
||||||
|
final TiledMapDocument map = codec.readMap(tmxPath);
|
||||||
|
assertEquals("../../tilesets/overworld/tileset.tsx", map.tilesets().getFirst().source());
|
||||||
|
assertEquals(16, map.width());
|
||||||
|
assertEquals(12, map.height());
|
||||||
|
assertEquals("Ground", map.tileLayers().getFirst().name());
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetWorkspaceAssetDetails tilesetDetails(Path assetRoot) {
|
||||||
|
return new AssetWorkspaceAssetDetails(
|
||||||
|
new AssetWorkspaceAssetSummary(
|
||||||
|
AssetReference.forAssetId(1),
|
||||||
|
"overworld_tileset",
|
||||||
|
AssetWorkspaceAssetState.REGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||||
|
1,
|
||||||
|
AssetFamilyCatalog.GLYPH_BANK,
|
||||||
|
AssetStudioGlyphSpecialization.TILESET,
|
||||||
|
assetRoot,
|
||||||
|
false,
|
||||||
|
false),
|
||||||
|
List.of(),
|
||||||
|
OutputFormatCatalog.GLYPH_INDEXED_V1,
|
||||||
|
OutputCodecCatalog.NONE,
|
||||||
|
List.of(OutputCodecCatalog.NONE),
|
||||||
|
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||||
|
List.of(new PackerCodecConfigurationFieldDTO(
|
||||||
|
"tile_size",
|
||||||
|
"Tile Size",
|
||||||
|
PackerCodecConfigurationFieldType.ENUM,
|
||||||
|
"16x16",
|
||||||
|
true,
|
||||||
|
List.of("8x8", "16x16", "32x32"))),
|
||||||
|
Map.of(),
|
||||||
|
new AssetWorkspaceBankCompositionDetails(
|
||||||
|
List.of(),
|
||||||
|
List.of(
|
||||||
|
new AssetWorkspaceBankCompositionFile("a.png", "a.png", 1L, 1L, null, Map.of()),
|
||||||
|
new AssetWorkspaceBankCompositionFile("b.png", "b.png", 1L, 1L, null, Map.of())),
|
||||||
|
0L),
|
||||||
|
List.of(),
|
||||||
|
null,
|
||||||
|
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||||
|
List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetWorkspaceAssetDetails sceneDetails(Path assetRoot) {
|
||||||
|
return new AssetWorkspaceAssetDetails(
|
||||||
|
new AssetWorkspaceAssetSummary(
|
||||||
|
AssetReference.forAssetId(2),
|
||||||
|
"overworld_scene",
|
||||||
|
AssetWorkspaceAssetState.REGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||||
|
2,
|
||||||
|
AssetFamilyCatalog.SCENE_BANK,
|
||||||
|
AssetStudioGlyphSpecialization.NONE,
|
||||||
|
assetRoot,
|
||||||
|
false,
|
||||||
|
false),
|
||||||
|
List.of(),
|
||||||
|
OutputFormatCatalog.SCENE_TILED_V1,
|
||||||
|
OutputCodecCatalog.NONE,
|
||||||
|
List.of(OutputCodecCatalog.NONE),
|
||||||
|
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||||
|
List.of(),
|
||||||
|
Map.of(),
|
||||||
|
new AssetWorkspaceBankCompositionDetails(List.of(), List.of(), 0L),
|
||||||
|
List.of(),
|
||||||
|
new AssetStudioSceneBankMetadata(
|
||||||
|
16,
|
||||||
|
12,
|
||||||
|
1,
|
||||||
|
List.of(new AssetStudioSceneLayerBinding(1, "Ground", "ground.tmx", "tilesets/overworld")),
|
||||||
|
assetRoot.resolve("scene-bank.studio.json")),
|
||||||
|
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||||
|
List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
final class TiledXmlCodecTest {
|
||||||
|
private final TiledXmlCodec codec = new TiledXmlCodec();
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readsFixtureTmxAndPreservesWaveOneSurfaces() throws Exception {
|
||||||
|
final TiledMapDocument map = codec.readMap(Path.of("..", "test-projects", "main", "assets", "scenes", "primeiro mapa.tmx").toAbsolutePath().normalize());
|
||||||
|
|
||||||
|
assertEquals(25, map.width());
|
||||||
|
assertEquals(25, map.height());
|
||||||
|
assertEquals(1, map.tilesets().size());
|
||||||
|
assertEquals("../Zelda3/primeiro tileset.tsx", map.tilesets().getFirst().source());
|
||||||
|
assertEquals(1, map.tileLayers().size());
|
||||||
|
assertEquals(625, map.tileLayers().getFirst().gids().size());
|
||||||
|
assertEquals(1, map.objectLayers().size());
|
||||||
|
assertEquals(9, map.objectLayers().getFirst().objects().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readsFixtureTsxWithPropertiesAndCollisionObjects() throws Exception {
|
||||||
|
final TiledTilesetDocument tileset = codec.readTileset(Path.of("..", "test-projects", "main", "assets", "Zelda3", "primeiro tileset.tsx").toAbsolutePath().normalize());
|
||||||
|
|
||||||
|
assertEquals(32, tileset.tileWidth());
|
||||||
|
assertEquals(32, tileset.tileHeight());
|
||||||
|
assertEquals(13, tileset.tileCount());
|
||||||
|
assertEquals(13, tileset.tiles().size());
|
||||||
|
assertEquals("glyph_id", tileset.tiles().getFirst().properties().getFirst().name());
|
||||||
|
assertNotNull(tileset.tiles().getFirst().collisionLayer());
|
||||||
|
assertEquals(2, tileset.tiles().getFirst().collisionLayer().objects().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsUnsupportedInfiniteMaps() throws Exception {
|
||||||
|
final Path path = tempDir.resolve("infinite.tmx");
|
||||||
|
Files.writeString(path, """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down"
|
||||||
|
width="10" height="10" tilewidth="16" tileheight="16" infinite="1"/>
|
||||||
|
""");
|
||||||
|
|
||||||
|
assertThrows(TiledUnsupportedFeatureException.class, () -> codec.readMap(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema_version" : 1,
|
"schema_version" : 1,
|
||||||
"next_asset_id" : 16,
|
"next_asset_id" : 17,
|
||||||
"assets" : [ {
|
"assets" : [ {
|
||||||
"asset_id" : 3,
|
"asset_id" : 3,
|
||||||
"asset_uuid" : "21953cb8-4101-4790-9e5e-d95f5fbc9b5a",
|
"asset_uuid" : "21953cb8-4101-4790-9e5e-d95f5fbc9b5a",
|
||||||
@ -41,5 +41,10 @@
|
|||||||
"asset_uuid" : "87396aab-337e-479e-b1f4-ec296678389e",
|
"asset_uuid" : "87396aab-337e-479e-b1f4-ec296678389e",
|
||||||
"root" : "Zelda3",
|
"root" : "Zelda3",
|
||||||
"included_in_build" : true
|
"included_in_build" : true
|
||||||
|
}, {
|
||||||
|
"asset_id" : 16,
|
||||||
|
"asset_uuid" : "6f05bee1-f974-4b65-a43f-eacd46b8ec96",
|
||||||
|
"root" : "tiled",
|
||||||
|
"included_in_build" : true
|
||||||
} ]
|
} ]
|
||||||
}
|
}
|
||||||
14
test-projects/main/assets/tiled/asset.json
Normal file
14
test-projects/main/assets/tiled/asset.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"schema_version" : 1,
|
||||||
|
"asset_uuid" : "6f05bee1-f974-4b65-a43f-eacd46b8ec96",
|
||||||
|
"name" : "Tiled Test",
|
||||||
|
"type" : "scene_bank",
|
||||||
|
"output" : {
|
||||||
|
"pipeline" : { },
|
||||||
|
"codec" : "NONE",
|
||||||
|
"format" : "SCENE/tiled_v1"
|
||||||
|
},
|
||||||
|
"preload" : {
|
||||||
|
"enabled" : false
|
||||||
|
}
|
||||||
|
}
|
||||||
12
test-projects/main/assets/tiled/scene-bank.studio.json
Normal file
12
test-projects/main/assets/tiled/scene-bank.studio.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"schema_version" : 1,
|
||||||
|
"map_width" : 16,
|
||||||
|
"map_height" : 16,
|
||||||
|
"layer_count" : 1,
|
||||||
|
"layers" : [ {
|
||||||
|
"index" : 1,
|
||||||
|
"tilemap" : "layer-1.tmx",
|
||||||
|
"tileset_asset_root" : "",
|
||||||
|
"name" : "Layer 1"
|
||||||
|
} ]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user