Compare commits
4 Commits
ea8c81368b
...
a6b0760657
| Author | SHA1 | Date | |
|---|---|---|---|
| a6b0760657 | |||
| b77a24c57b | |||
| 831e419c80 | |||
| bbd588eed4 |
@ -4,6 +4,7 @@ import java.util.Locale;
|
||||
|
||||
public enum AssetFamilyCatalog {
|
||||
GLYPH_BANK("glyph_bank"),
|
||||
SCENE_BANK("scene_bank"),
|
||||
SOUND_BANK("sound_bank"),
|
||||
UNKNOWN("unknown");
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import java.util.Locale;
|
||||
|
||||
public enum OutputFormatCatalog {
|
||||
GLYPH_INDEXED_V1(AssetFamilyCatalog.GLYPH_BANK, "GLYPH/indexed_v1", "GLYPH/indexed_v1"),
|
||||
SCENE_TILED_V1(AssetFamilyCatalog.SCENE_BANK, "SCENE/tiled_v1", "SCENE/tiled_v1"),
|
||||
SOUND_V1(AssetFamilyCatalog.SOUND_BANK, "SOUND/v1", "SOUND/v1"),
|
||||
UNKNOWN(AssetFamilyCatalog.UNKNOWN, "unknown", "Unknown");
|
||||
|
||||
|
||||
@ -100,6 +100,9 @@ public class PackerAssetWalker {
|
||||
diagnostics.addAll(walkResult.diagnostics());
|
||||
return new PackerWalkResult(walkResult.probeResults(), diagnostics);
|
||||
}
|
||||
case SCENE_BANK -> {
|
||||
return new PackerWalkResult(List.of(), diagnostics);
|
||||
}
|
||||
case UNKNOWN -> {
|
||||
diagnostics.add(new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.WARNING,
|
||||
|
||||
@ -38,6 +38,7 @@ import java.util.stream.Stream;
|
||||
|
||||
public final class FileSystemPackerWorkspaceService implements PackerWorkspaceService {
|
||||
private static final int GLYPH_BANK_COLOR_KEY_RGB565 = 0xF81F;
|
||||
private static final String SCENE_BANK_SUPPORT_FILE = "scene-bank.studio.json";
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
private final PackerWorkspaceFoundation workspaceFoundation;
|
||||
@ -287,6 +288,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
final PackerRegistryEntry entry = workspaceFoundation.allocateIdentity(project, registry, assetRoot);
|
||||
Files.createDirectories(assetRoot);
|
||||
writeManifest(manifestPath, request, entry.assetUuid());
|
||||
writeStudioSupportFiles(assetRoot, request);
|
||||
final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry);
|
||||
workspaceFoundation.saveRegistry(project, updated);
|
||||
final var runtime = runtimeRegistry.update(project, (snapshot, generation) -> runtimePatchService.afterCreateAsset(
|
||||
@ -667,6 +669,25 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
|
||||
}
|
||||
|
||||
private void writeStudioSupportFiles(Path assetRoot, CreateAssetRequest request) throws IOException {
|
||||
if (request.assetFamily() != AssetFamilyCatalog.SCENE_BANK) {
|
||||
return;
|
||||
}
|
||||
final Map<String, Object> supportFile = new LinkedHashMap<>();
|
||||
supportFile.put("schema_version", 1);
|
||||
supportFile.put("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) {
|
||||
final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/');
|
||||
if (raw.isBlank()) {
|
||||
|
||||
@ -121,7 +121,7 @@ public final class PackerAssetDeclarationParser {
|
||||
diagnostics.add(new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.ERROR,
|
||||
PackerDiagnosticCategory.STRUCTURAL,
|
||||
"Field 'type' must be one of: glyph_bank, palette_bank, sound_bank.",
|
||||
"Field 'type' must be one of: glyph_bank, scene_bank, sound_bank.",
|
||||
manifestPath,
|
||||
true));
|
||||
return null;
|
||||
|
||||
@ -512,6 +512,32 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.ACTION_APPLIED));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsSceneBankAssetAndWritesStudioSupportFile() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("created-scene-bank");
|
||||
final FileSystemPackerWorkspaceService service = service();
|
||||
|
||||
final var result = service.createAsset(new CreateAssetRequest(
|
||||
project(projectRoot),
|
||||
"scenes/overworld",
|
||||
"overworld",
|
||||
AssetFamilyCatalog.SCENE_BANK,
|
||||
OutputFormatCatalog.SCENE_TILED_V1,
|
||||
OutputCodecCatalog.NONE,
|
||||
false));
|
||||
|
||||
assertEquals(PackerOperationStatus.SUCCESS, result.status());
|
||||
final Path assetRoot = projectRoot.resolve("assets/scenes/overworld");
|
||||
assertTrue(Files.isRegularFile(assetRoot.resolve("asset.json")));
|
||||
assertTrue(Files.isRegularFile(assetRoot.resolve("scene-bank.studio.json")));
|
||||
final var supportFile = MAPPER.readTree(assetRoot.resolve("scene-bank.studio.json").toFile());
|
||||
assertEquals(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
|
||||
void returnsCreatedAssetThroughRuntimeBackedDetailsWithoutRescanMismatch() throws Exception {
|
||||
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());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesSceneBankDeclaration() throws Exception {
|
||||
final Path manifest = tempDir.resolve("scene-asset.json");
|
||||
Files.writeString(manifest, """
|
||||
{
|
||||
"schema_version": 1,
|
||||
"asset_uuid": "uuid-scene",
|
||||
"name": "overworld_scene",
|
||||
"type": "scene_bank",
|
||||
"output": {
|
||||
"format": "SCENE/tiled_v1",
|
||||
"codec": "NONE"
|
||||
},
|
||||
"preload": { "enabled": false }
|
||||
}
|
||||
""");
|
||||
|
||||
final var result = parser.parse(manifest);
|
||||
|
||||
assertTrue(result.valid());
|
||||
assertEquals(AssetFamilyCatalog.SCENE_BANK, result.declaration().assetFamily());
|
||||
assertEquals("SCENE/tiled_v1", result.declaration().outputFormat().displayName());
|
||||
assertEquals(OutputCodecCatalog.NONE, result.declaration().outputCodec());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNonObjectPipelineMetadata() throws Exception {
|
||||
final Path manifest = tempDir.resolve("asset.json");
|
||||
|
||||
@ -154,6 +154,10 @@ public enum I18n {
|
||||
ASSETS_ACTIONS_EMPTY("assets.actions.empty"),
|
||||
ASSETS_ACTION_REGISTER("assets.action.register"),
|
||||
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_INCLUDE_IN_BUILD("assets.action.includeInBuild"),
|
||||
ASSETS_ACTION_EXCLUDE_FROM_BUILD("assets.action.excludeFromBuild"),
|
||||
@ -190,10 +194,24 @@ public enum I18n {
|
||||
ASSETS_LABEL_BUILD_PARTICIPATION("assets.label.buildParticipation"),
|
||||
ASSETS_LABEL_ASSET_ID("assets.label.assetId"),
|
||||
ASSETS_LABEL_TYPE("assets.label.type"),
|
||||
ASSETS_LABEL_STUDIO_ROLE("assets.label.studioRole"),
|
||||
ASSETS_LABEL_SCENE_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_SCENE_BANK("assets.type.sceneBank"),
|
||||
ASSETS_TYPE_PALETTE_BANK("assets.type.paletteBank"),
|
||||
ASSETS_TYPE_SOUND_BANK("assets.type.soundBank"),
|
||||
ASSETS_TYPE_UNKNOWN("assets.type.unknown"),
|
||||
ASSETS_SPECIALIZATION_NONE("assets.specialization.none"),
|
||||
ASSETS_SPECIALIZATION_TILESET("assets.specialization.tileset"),
|
||||
ASSETS_SPECIALIZATION_SPRITES("assets.specialization.sprites"),
|
||||
ASSETS_SPECIALIZATION_UI("assets.specialization.ui"),
|
||||
ASSETS_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_BANK("assets.label.bank"),
|
||||
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_ROOT("assets.addWizard.label.root"),
|
||||
ASSETS_ADD_WIZARD_LABEL_TYPE("assets.addWizard.label.type"),
|
||||
ASSETS_ADD_WIZARD_LABEL_SPECIALIZATION("assets.addWizard.label.specialization"),
|
||||
ASSETS_ADD_WIZARD_LABEL_FORMAT("assets.addWizard.label.format"),
|
||||
ASSETS_ADD_WIZARD_LABEL_CODEC("assets.addWizard.label.codec"),
|
||||
ASSETS_ADD_WIZARD_LABEL_PRELOAD("assets.addWizard.label.preload"),
|
||||
ASSETS_ADD_WIZARD_PROMPT_TYPE("assets.addWizard.prompt.type"),
|
||||
ASSETS_ADD_WIZARD_PROMPT_SPECIALIZATION("assets.addWizard.prompt.specialization"),
|
||||
ASSETS_ADD_WIZARD_PROMPT_FORMAT("assets.addWizard.prompt.format"),
|
||||
ASSETS_ADD_WIZARD_PROMPT_CODEC("assets.addWizard.prompt.codec"),
|
||||
ASSETS_ADD_WIZARD_ASSETS_ROOT_HINT("assets.addWizard.assetsRootHint"),
|
||||
|
||||
@ -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.palette.AssetDetailsPaletteOverhaulingControl;
|
||||
import p.studio.workspaces.assets.details.summary.AssetDetailsSummaryControl;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioMetadataSnapshot;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetAction;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionDetails;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||
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.AssetWorkspaceDetailsViewState;
|
||||
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.StudioAssetsRefreshRequestedEvent;
|
||||
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.MoveAssetWizard;
|
||||
import p.studio.workspaces.framework.StudioEventAware;
|
||||
import p.studio.workspaces.framework.StudioEventBindings;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@ -53,6 +63,9 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
private final AssetDetailsContractControl contractControl;
|
||||
private final AssetDetailsBankCompositionControl bankCompositionControl;
|
||||
private final AssetDetailsPaletteOverhaulingControl paletteOverhaulingControl;
|
||||
private final AssetStudioMetadataService studioMetadataService = new AssetStudioMetadataService();
|
||||
private final TiledAssetGenerationService tiledGenerationService = new TiledAssetGenerationService();
|
||||
private final SceneBankWorkflowService sceneBankWorkflowService = new SceneBankWorkflowService();
|
||||
private final VBox actionsContent = new VBox(10);
|
||||
private final ScrollPane actionsScroll = new ScrollPane();
|
||||
private final VBox actionsSection;
|
||||
@ -324,6 +337,29 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
analyseButton.setDisable(actionRunning || viewState.selectedAssetDetails().diagnostics().isEmpty());
|
||||
analyseButton.setOnAction(ignored -> openDiagnosticsDialog());
|
||||
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());
|
||||
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) {
|
||||
actionRunning = false;
|
||||
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,
|
||||
java.util.List<PackerDiagnosticDTO> diagnostics,
|
||||
java.util.List<PackerAssetActionAvailabilityDTO> actions) {
|
||||
final java.util.List<PackerDiagnosticDTO> mergedDiagnostics = new java.util.ArrayList<>(details.diagnostics());
|
||||
for (PackerDiagnosticDTO diagnostic : diagnostics) {
|
||||
if (!mergedDiagnostics.contains(diagnostic)) {
|
||||
mergedDiagnostics.add(diagnostic);
|
||||
}
|
||||
}
|
||||
return new AssetWorkspaceAssetDetails(
|
||||
AssetListPackerMappings.mapSummary(details.summary()),
|
||||
actions.stream()
|
||||
.map(action -> new AssetWorkspaceAssetAction(
|
||||
action.action(),
|
||||
action.enabled(),
|
||||
action.visible(),
|
||||
action.reason()))
|
||||
.toList(),
|
||||
final var baseSummary = AssetListPackerMappings.mapSummary(details.summary());
|
||||
final AssetStudioMetadataSnapshot studioMetadata = studioMetadataService.read(
|
||||
baseSummary.assetRoot(),
|
||||
baseSummary.assetFamily());
|
||||
final java.util.List<AssetWorkspaceAssetAction> mappedActions = actions.stream()
|
||||
.map(action -> new AssetWorkspaceAssetAction(
|
||||
action.action(),
|
||||
action.enabled(),
|
||||
action.visible(),
|
||||
action.reason()))
|
||||
.toList();
|
||||
final p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary mappedSummary =
|
||||
new p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary(
|
||||
baseSummary.assetReference(),
|
||||
baseSummary.assetName(),
|
||||
baseSummary.state(),
|
||||
baseSummary.buildParticipation(),
|
||||
baseSummary.assetId(),
|
||||
baseSummary.assetFamily(),
|
||||
studioMetadata.glyphSpecialization(),
|
||||
baseSummary.assetRoot(),
|
||||
baseSummary.preload(),
|
||||
baseSummary.hasDiagnostics());
|
||||
final AssetWorkspaceBankCompositionDetails bankComposition = mapBankComposition(details.bankComposition());
|
||||
final AssetWorkspaceAssetDetails draftDetails = new AssetWorkspaceAssetDetails(
|
||||
mappedSummary,
|
||||
mappedActions,
|
||||
details.outputFormat(),
|
||||
details.outputCodec(),
|
||||
details.availableOutputCodecs(),
|
||||
details.codecConfigurationFieldsByCodec(),
|
||||
details.metadataFields(),
|
||||
details.outputPipeline(),
|
||||
mapBankComposition(details.bankComposition()),
|
||||
bankComposition,
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,8 @@ import p.studio.controls.forms.StudioFormSection;
|
||||
import p.studio.controls.forms.StudioSection;
|
||||
import p.studio.projects.ProjectReference;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceSceneBankStatus;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetState;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation;
|
||||
|
||||
@ -114,6 +116,7 @@ public final class AssetDetailsUiSupport {
|
||||
public static String typeLabel(AssetFamilyCatalog assetFamily) {
|
||||
return switch (assetFamily) {
|
||||
case GLYPH_BANK -> Container.i18n().text(I18n.ASSETS_TYPE_GLYPH_BANK);
|
||||
case SCENE_BANK -> Container.i18n().text(I18n.ASSETS_TYPE_SCENE_BANK);
|
||||
case SOUND_BANK -> Container.i18n().text(I18n.ASSETS_TYPE_SOUND_BANK);
|
||||
case UNKNOWN -> Container.i18n().text(I18n.ASSETS_TYPE_UNKNOWN);
|
||||
};
|
||||
@ -122,11 +125,31 @@ public final class AssetDetailsUiSupport {
|
||||
public static String typeChipTone(AssetFamilyCatalog assetFamily) {
|
||||
return switch (assetFamily) {
|
||||
case GLYPH_BANK -> "assets-details-chip-image";
|
||||
case SCENE_BANK -> "assets-details-chip-generic";
|
||||
case SOUND_BANK -> "assets-details-chip-audio";
|
||||
case UNKNOWN -> "assets-details-chip-generic";
|
||||
};
|
||||
}
|
||||
|
||||
public static String specializationLabel(AssetStudioGlyphSpecialization specialization) {
|
||||
return switch (specialization) {
|
||||
case NONE -> Container.i18n().text(I18n.ASSETS_SPECIALIZATION_NONE);
|
||||
case TILESET -> Container.i18n().text(I18n.ASSETS_SPECIALIZATION_TILESET);
|
||||
case SPRITES -> Container.i18n().text(I18n.ASSETS_SPECIALIZATION_SPRITES);
|
||||
case UI -> Container.i18n().text(I18n.ASSETS_SPECIALIZATION_UI);
|
||||
};
|
||||
}
|
||||
|
||||
public static String 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) {
|
||||
return switch (action) {
|
||||
case REGISTER -> Container.i18n().text(I18n.ASSETS_ACTION_REGISTER);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package p.studio.workspaces.assets.details;
|
||||
|
||||
import p.packer.dtos.PackerAssetSummaryDTO;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetState;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation;
|
||||
@ -25,6 +26,7 @@ public final class AssetListPackerMappings {
|
||||
buildParticipation,
|
||||
summary.identity().assetId(),
|
||||
summary.assetFamily(),
|
||||
AssetStudioGlyphSpecialization.NONE,
|
||||
summary.identity().assetRoot(),
|
||||
summary.preloadEnabled(),
|
||||
summary.hasDiagnostics());
|
||||
|
||||
@ -21,6 +21,12 @@ public final class AssetBankCapacityService {
|
||||
final Map<String, Object> safePipeline = Map.copyOf(Objects.requireNonNull(outputPipeline, "outputPipeline"));
|
||||
return switch (safeFamily) {
|
||||
case GLYPH_BANK -> evaluateGlyphBank(artifactCount, safeMetadata);
|
||||
case SCENE_BANK -> new AssetDetailsBankCompositionCapacityState(
|
||||
0.0d,
|
||||
StudioAssetCapacitySeverity.GREEN,
|
||||
false,
|
||||
artifactCount + " support files",
|
||||
"");
|
||||
case SOUND_BANK -> evaluateSoundBank(resolveSoundBankUsedBytes(safePipeline, usedBytes));
|
||||
case UNKNOWN -> new AssetDetailsBankCompositionCapacityState(
|
||||
0.0d,
|
||||
|
||||
@ -8,6 +8,9 @@ import p.studio.lsp.events.StudioWorkspaceEventBus;
|
||||
import p.studio.projects.ProjectReference;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
import p.studio.workspaces.assets.details.AssetDetailsUiSupport;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
||||
@ -53,14 +56,43 @@ public final class AssetDetailsSummaryControl extends VBox implements StudioCont
|
||||
}
|
||||
|
||||
final VBox content = new VBox(8);
|
||||
final AssetStudioSceneBankMetadata sceneBankMetadata = viewState.selectedAssetDetails().sceneBankMetadata();
|
||||
content.getChildren().setAll(
|
||||
AssetDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_ASSET_ID), summary.assetId() == null ? "—" : String.valueOf(summary.assetId())),
|
||||
AssetDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), AssetDetailsUiSupport.projectRelativePath(projectReference, summary.assetRoot())));
|
||||
final VBox typeBox = new VBox(6);
|
||||
typeBox.getChildren().add(AssetDetailsUiSupport.createChip(
|
||||
AssetDetailsUiSupport.typeChipTone(summary.assetFamily()),
|
||||
AssetDetailsUiSupport.typeLabel(summary.assetFamily())));
|
||||
if (summary.glyphSpecialization() != AssetStudioGlyphSpecialization.NONE) {
|
||||
typeBox.getChildren().add(AssetDetailsUiSupport.createChip(
|
||||
"assets-details-chip-generic",
|
||||
AssetDetailsUiSupport.specializationLabel(summary.glyphSpecialization())));
|
||||
}
|
||||
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_TYPE),
|
||||
AssetDetailsUiSupport.createChip(
|
||||
AssetDetailsUiSupport.typeChipTone(summary.assetFamily()),
|
||||
AssetDetailsUiSupport.typeLabel(summary.assetFamily()))));
|
||||
typeBox));
|
||||
if (summary.glyphSpecialization() != AssetStudioGlyphSpecialization.NONE) {
|
||||
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_STUDIO_ROLE),
|
||||
AssetDetailsUiSupport.specializationLabel(summary.glyphSpecialization())));
|
||||
}
|
||||
if (sceneBankMetadata != null) {
|
||||
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_SCENE_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(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_REGISTRATION),
|
||||
AssetDetailsUiSupport.createChip(
|
||||
|
||||
@ -113,6 +113,7 @@ public final class AssetListItemControl extends VBox {
|
||||
private String assetRowToneClass(AssetFamilyCatalog assetFamily) {
|
||||
return switch (assetFamily) {
|
||||
case GLYPH_BANK -> "assets-workspace-asset-row-tone-image";
|
||||
case SCENE_BANK -> "assets-workspace-asset-row-tone-generic";
|
||||
case SOUND_BANK -> "assets-workspace-asset-row-tone-audio";
|
||||
default -> "assets-workspace-asset-row-tone-generic";
|
||||
};
|
||||
@ -121,6 +122,7 @@ public final class AssetListItemControl extends VBox {
|
||||
private String assetNameToneClass(AssetFamilyCatalog assetFamily) {
|
||||
return switch (assetFamily) {
|
||||
case GLYPH_BANK -> "assets-workspace-asset-name-tone-image";
|
||||
case SCENE_BANK -> "assets-workspace-asset-name-tone-generic";
|
||||
case SOUND_BANK -> "assets-workspace-asset-name-tone-audio";
|
||||
default -> "assets-workspace-asset-name-tone-generic";
|
||||
};
|
||||
|
||||
@ -4,6 +4,7 @@ import p.packer.dtos.PackerCodecConfigurationFieldDTO;
|
||||
import p.packer.dtos.PackerDiagnosticDTO;
|
||||
import p.packer.messages.assets.OutputCodecCatalog;
|
||||
import p.packer.messages.assets.OutputFormatCatalog;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -20,6 +21,8 @@ public record AssetWorkspaceAssetDetails(
|
||||
Map<String, Object> outputPipeline,
|
||||
AssetWorkspaceBankCompositionDetails bankComposition,
|
||||
List<Map<String, Object>> pipelinePalettes,
|
||||
AssetStudioSceneBankMetadata sceneBankMetadata,
|
||||
AssetWorkspaceSceneBankValidation sceneBankValidation,
|
||||
List<PackerDiagnosticDTO> diagnostics) {
|
||||
|
||||
public AssetWorkspaceAssetDetails {
|
||||
@ -33,6 +36,7 @@ public record AssetWorkspaceAssetDetails(
|
||||
outputPipeline = Map.copyOf(Objects.requireNonNull(outputPipeline, "outputPipeline"));
|
||||
bankComposition = Objects.requireNonNull(bankComposition, "bankComposition");
|
||||
pipelinePalettes = List.copyOf(Objects.requireNonNull(pipelinePalettes, "pipelinePalettes"));
|
||||
sceneBankValidation = Objects.requireNonNullElse(sceneBankValidation, AssetWorkspaceSceneBankValidation.NOT_APPLICABLE);
|
||||
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.assets.AssetFamilyCatalog;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
@ -13,6 +14,7 @@ public record AssetWorkspaceAssetSummary(
|
||||
AssetWorkspaceBuildParticipation buildParticipation,
|
||||
Integer assetId,
|
||||
AssetFamilyCatalog assetFamily,
|
||||
AssetStudioGlyphSpecialization glyphSpecialization,
|
||||
Path assetRoot,
|
||||
boolean preload,
|
||||
boolean hasDiagnostics) {
|
||||
@ -23,6 +25,7 @@ public record AssetWorkspaceAssetSummary(
|
||||
Objects.requireNonNull(state, "state");
|
||||
Objects.requireNonNull(buildParticipation, "buildParticipation");
|
||||
assetFamily = Objects.requireNonNullElse(assetFamily, AssetFamilyCatalog.UNKNOWN);
|
||||
glyphSpecialization = Objects.requireNonNullElse(glyphSpecialization, AssetStudioGlyphSpecialization.NONE);
|
||||
assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
||||
if (assetName.isBlank()) {
|
||||
throw new IllegalArgumentException("assetName must not be blank");
|
||||
|
||||
@ -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.utilities.i18n.I18n;
|
||||
import p.studio.workspaces.assets.details.AssetDetailsUiSupport;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@ -47,9 +49,11 @@ public final class AddAssetWizard {
|
||||
private final TextField assetRootField = new TextField();
|
||||
private final TextField assetNameField = new TextField();
|
||||
private final ComboBox<AssetFamilyCatalog> assetFamilyCombo = new ComboBox<>();
|
||||
private final ComboBox<AssetStudioGlyphSpecialization> glyphSpecializationCombo = new ComboBox<>();
|
||||
private final ComboBox<OutputFormatCatalog> outputFormatCombo = new ComboBox<>();
|
||||
private final ComboBox<OutputCodecCatalog> outputCodecCombo = new ComboBox<>();
|
||||
private final CheckBox preloadCheckBox = new CheckBox();
|
||||
private final AssetStudioMetadataService studioMetadataService = new AssetStudioMetadataService();
|
||||
|
||||
private int stepIndex;
|
||||
private boolean creating;
|
||||
@ -65,6 +69,7 @@ public final class AddAssetWizard {
|
||||
|
||||
preloadCheckBox.setSelected(false);
|
||||
configureAssetFamilyCombo();
|
||||
configureGlyphSpecializationCombo();
|
||||
configureOutputFormatCombo();
|
||||
configureOutputCodecCombo();
|
||||
renderStep();
|
||||
@ -131,7 +136,34 @@ public final class AddAssetWizard {
|
||||
});
|
||||
assetFamilyCombo.valueProperty().addListener((ignored, oldValue, newValue) -> {
|
||||
if (!Objects.equals(oldValue, newValue)) {
|
||||
if (newValue != AssetFamilyCatalog.GLYPH_BANK) {
|
||||
glyphSpecializationCombo.getSelectionModel().select(AssetStudioGlyphSpecialization.NONE);
|
||||
}
|
||||
refreshOutputFormats();
|
||||
if (stepIndex == 1) {
|
||||
renderStep();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void configureGlyphSpecializationCombo() {
|
||||
glyphSpecializationCombo.setItems(FXCollections.observableArrayList(AssetStudioGlyphSpecialization.values()));
|
||||
glyphSpecializationCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
glyphSpecializationCombo.setPromptText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_PROMPT_SPECIALIZATION));
|
||||
glyphSpecializationCombo.getSelectionModel().select(AssetStudioGlyphSpecialization.NONE);
|
||||
glyphSpecializationCombo.setCellFactory(ignored -> new javafx.scene.control.ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(AssetStudioGlyphSpecialization item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
setText(empty || item == null ? null : AssetDetailsUiSupport.specializationLabel(item));
|
||||
}
|
||||
});
|
||||
glyphSpecializationCombo.setButtonCell(new javafx.scene.control.ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(AssetStudioGlyphSpecialization item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
setText(empty || item == null ? null : AssetDetailsUiSupport.specializationLabel(item));
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -224,17 +256,23 @@ public final class AddAssetWizard {
|
||||
|
||||
final Label nameLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_NAME));
|
||||
final Label typeLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_TYPE));
|
||||
final Label specializationLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_SPECIALIZATION));
|
||||
final Label formatLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_FORMAT));
|
||||
final Label codecLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_CODEC));
|
||||
final Label preloadLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_PRELOAD));
|
||||
final Label noteLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_NOTE));
|
||||
noteLabel.setWrapText(true);
|
||||
noteLabel.getStyleClass().add("studio-launcher-subtitle");
|
||||
final VBox specializationBox = new VBox(6, specializationLabel, glyphSpecializationCombo);
|
||||
final boolean specializationVisible = selectedFamily() == AssetFamilyCatalog.GLYPH_BANK;
|
||||
specializationBox.setVisible(specializationVisible);
|
||||
specializationBox.setManaged(specializationVisible);
|
||||
|
||||
preloadCheckBox.setText("");
|
||||
stepBody.getChildren().setAll(
|
||||
new VBox(6, nameLabel, assetNameField),
|
||||
new VBox(6, typeLabel, assetFamilyCombo),
|
||||
specializationBox,
|
||||
new VBox(6, formatLabel, outputFormatCombo),
|
||||
new VBox(6, codecLabel, outputCodecCombo),
|
||||
new VBox(6, preloadLabel, preloadCheckBox),
|
||||
@ -360,6 +398,15 @@ public final class AddAssetWizard {
|
||||
private void applyCreateResult(CreateAssetResult createResult) {
|
||||
creating = false;
|
||||
if (createResult.status() == PackerOperationStatus.SUCCESS && createResult.assetReference() != null) {
|
||||
try {
|
||||
persistStudioMetadata(createResult.assetRoot());
|
||||
} catch (IOException exception) {
|
||||
feedbackLabel.setText(exception.getMessage() == null || exception.getMessage().isBlank()
|
||||
? "Unable to persist Studio metadata."
|
||||
: exception.getMessage());
|
||||
renderStep();
|
||||
return;
|
||||
}
|
||||
result.set(createResult.assetReference());
|
||||
stage.close();
|
||||
return;
|
||||
@ -409,6 +456,17 @@ public final class AddAssetWizard {
|
||||
return outputCodecCombo.getValue();
|
||||
}
|
||||
|
||||
private AssetStudioGlyphSpecialization selectedGlyphSpecialization() {
|
||||
return glyphSpecializationCombo.getValue();
|
||||
}
|
||||
|
||||
private void persistStudioMetadata(Path assetRoot) throws IOException {
|
||||
if (selectedFamily() != AssetFamilyCatalog.GLYPH_BANK) {
|
||||
return;
|
||||
}
|
||||
studioMetadataService.writeGlyphSpecialization(assetRoot, selectedGlyphSpecialization());
|
||||
}
|
||||
|
||||
private String normalizedRelativeRoot(String candidate) {
|
||||
final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/');
|
||||
if (raw.isBlank()) {
|
||||
|
||||
@ -144,6 +144,10 @@ assets.section.actions=Actions
|
||||
assets.actions.empty=No actions available for this asset.
|
||||
assets.action.register=Register
|
||||
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.deleteDialog.title=Delete Asset
|
||||
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.assetId=Asset ID
|
||||
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.sceneBank=Scene Bank
|
||||
assets.type.paletteBank=Palette Bank
|
||||
assets.type.soundBank=Sound Bank
|
||||
assets.type.unknown=Unknown
|
||||
assets.specialization.none=None
|
||||
assets.specialization.tileset=Tileset
|
||||
assets.specialization.sprites=Sprites
|
||||
assets.specialization.ui=UI
|
||||
assets.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.bank=Bank
|
||||
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.root=Asset Root
|
||||
assets.addWizard.label.type=Asset Type
|
||||
assets.addWizard.label.specialization=Studio Specialization
|
||||
assets.addWizard.label.format=Output Format
|
||||
assets.addWizard.label.codec=Output Codec
|
||||
assets.addWizard.label.preload=Preload on startup
|
||||
assets.addWizard.prompt.type=Choose asset type
|
||||
assets.addWizard.prompt.specialization=Choose Studio specialization
|
||||
assets.addWizard.prompt.format=Choose output format
|
||||
assets.addWizard.prompt.codec=Choose output codec
|
||||
assets.addWizard.assetsRootHint=Assets root: {0}
|
||||
|
||||
@ -7,6 +7,7 @@ import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.packer.messages.assets.OutputCodecCatalog;
|
||||
import p.packer.messages.assets.OutputFormatCatalog;
|
||||
import p.packer.messages.assets.PackerCodecConfigurationFieldType;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.messages.*;
|
||||
|
||||
import java.nio.file.Path;
|
||||
@ -114,6 +115,8 @@ final class AssetDetailsBankCompositionCoordinatorTest {
|
||||
List.of(),
|
||||
0L),
|
||||
List.of(),
|
||||
null,
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
}
|
||||
|
||||
@ -136,6 +139,8 @@ final class AssetDetailsBankCompositionCoordinatorTest {
|
||||
List.of(),
|
||||
0L),
|
||||
List.of(),
|
||||
null,
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
}
|
||||
|
||||
@ -159,6 +164,7 @@ final class AssetDetailsBankCompositionCoordinatorTest {
|
||||
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||
1,
|
||||
family,
|
||||
AssetStudioGlyphSpecialization.NONE,
|
||||
Path.of("/tmp/bank"),
|
||||
false,
|
||||
false);
|
||||
|
||||
@ -5,6 +5,7 @@ import p.packer.messages.AssetReference;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.packer.messages.assets.OutputCodecCatalog;
|
||||
import p.packer.messages.assets.OutputFormatCatalog;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.messages.*;
|
||||
|
||||
import java.nio.file.Path;
|
||||
@ -81,6 +82,7 @@ final class AssetDetailsPaletteOverhaulingCoordinatorTest {
|
||||
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||
1,
|
||||
AssetFamilyCatalog.GLYPH_BANK,
|
||||
AssetStudioGlyphSpecialization.NONE,
|
||||
Path.of("/tmp/bank"),
|
||||
false,
|
||||
false),
|
||||
@ -93,6 +95,8 @@ final class AssetDetailsPaletteOverhaulingCoordinatorTest {
|
||||
Map.of(),
|
||||
new AssetWorkspaceBankCompositionDetails(availableFiles, selectedFiles, 0L),
|
||||
selectedFiles.stream().map(file -> (Map<String, Object>) file.metadata().get("palette")).toList(),
|
||||
null,
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
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,
|
||||
"next_asset_id" : 16,
|
||||
"next_asset_id" : 17,
|
||||
"assets" : [ {
|
||||
"asset_id" : 3,
|
||||
"asset_uuid" : "21953cb8-4101-4790-9e5e-d95f5fbc9b5a",
|
||||
@ -41,5 +41,10 @@
|
||||
"asset_uuid" : "87396aab-337e-479e-b1f4-ec296678389e",
|
||||
"root" : "Zelda3",
|
||||
"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