implements PLN-0054
This commit is contained in:
parent
bcc5b413fa
commit
33fd7c485e
@ -675,10 +675,14 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
}
|
||||
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,
|
||||
"tilemap", "layer-1.tmx")));
|
||||
"name", "Layer 1",
|
||||
"tilemap", "layer-1.tmx",
|
||||
"tileset_asset_root", "")));
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(
|
||||
assetRoot.resolve(SCENE_BANK_SUPPORT_FILE).toFile(),
|
||||
supportFile);
|
||||
|
||||
@ -531,7 +531,10 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
@ -154,6 +154,8 @@ 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_DELETE("assets.action.delete"),
|
||||
ASSETS_ACTION_INCLUDE_IN_BUILD("assets.action.includeInBuild"),
|
||||
ASSETS_ACTION_EXCLUDE_FROM_BUILD("assets.action.excludeFromBuild"),
|
||||
|
||||
@ -25,6 +25,7 @@ import p.studio.workspaces.assets.details.palette.AssetDetailsPaletteOverhauling
|
||||
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;
|
||||
@ -35,6 +36,8 @@ import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation;
|
||||
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.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;
|
||||
@ -56,6 +59,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
private final AssetDetailsBankCompositionControl bankCompositionControl;
|
||||
private final AssetDetailsPaletteOverhaulingControl paletteOverhaulingControl;
|
||||
private final AssetStudioMetadataService studioMetadataService = new AssetStudioMetadataService();
|
||||
private final TiledAssetGenerationService tiledGenerationService = new TiledAssetGenerationService();
|
||||
private final VBox actionsContent = new VBox(10);
|
||||
private final ScrollPane actionsScroll = new ScrollPane();
|
||||
private final VBox actionsSection;
|
||||
@ -327,6 +331,18 @@ 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);
|
||||
}
|
||||
|
||||
final Button buildParticipationButton = AssetDetailsUiSupport.createActionButton(buildParticipationActionLabel());
|
||||
AssetDetailsUiSupport.applyActionTone(
|
||||
@ -452,6 +468,55 @@ 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 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();
|
||||
renderActions();
|
||||
if (result.success() && 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) {
|
||||
|
||||
@ -78,8 +78,10 @@ public final class AssetStudioMetadataService {
|
||||
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 (layerCount < 1 || layerCount > 4) {
|
||||
if (mapWidth <= 0 || mapHeight <= 0 || layerCount < 1 || layerCount > 4) {
|
||||
return null;
|
||||
}
|
||||
if (!(root.path("layers") instanceof ArrayNode layersNode) || layersNode.size() != layerCount) {
|
||||
@ -89,14 +91,16 @@ public final class AssetStudioMetadataService {
|
||||
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();
|
||||
if (index < 1 || index > layerCount || tilemap.isBlank() || !indexes.add(index)) {
|
||||
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, tilemap));
|
||||
bindings.add(new AssetStudioSceneLayerBinding(index, layerName, tilemap, tilesetAssetRoot));
|
||||
}
|
||||
bindings.sort(Comparator.comparingInt(AssetStudioSceneLayerBinding::index));
|
||||
return new AssetStudioSceneBankMetadata(layerCount, bindings, supportFile);
|
||||
return new AssetStudioSceneBankMetadata(mapWidth, mapHeight, layerCount, bindings, supportFile);
|
||||
} catch (IOException ignored) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetStudioSceneBankMetadata(
|
||||
int mapWidth,
|
||||
int mapHeight,
|
||||
int layerCount,
|
||||
List<AssetStudioSceneLayerBinding> layerBindings,
|
||||
Path supportFile) {
|
||||
@ -12,6 +14,9 @@ public record AssetStudioSceneBankMetadata(
|
||||
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");
|
||||
}
|
||||
|
||||
@ -2,12 +2,21 @@ package p.studio.workspaces.assets.metadata;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetStudioSceneLayerBinding(int index, String tilemap) {
|
||||
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,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);
|
||||
}
|
||||
}
|
||||
@ -144,6 +144,8 @@ 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.delete=Delete
|
||||
assets.deleteDialog.title=Delete Asset
|
||||
assets.deleteDialog.description=Type the asset name below to confirm deletion of {0}.
|
||||
|
||||
@ -34,10 +34,12 @@ final class AssetStudioMetadataServiceTest {
|
||||
Files.writeString(assetRoot.resolve(AssetStudioMetadataService.SCENE_BANK_SUPPORT_FILE), """
|
||||
{
|
||||
"schema_version": 1,
|
||||
"map_width": 20,
|
||||
"map_height": 12,
|
||||
"layer_count": 2,
|
||||
"layers": [
|
||||
{ "index": 1, "tilemap": "ground.tmx" },
|
||||
{ "index": 2, "tilemap": "collision.tmx" }
|
||||
{ "index": 1, "name": "Ground", "tilemap": "ground.tmx", "tileset_asset_root": "tilesets/ground" },
|
||||
{ "index": 2, "name": "Collision", "tilemap": "collision.tmx", "tileset_asset_root": "tilesets/ground" }
|
||||
]
|
||||
}
|
||||
""");
|
||||
@ -45,6 +47,8 @@ final class AssetStudioMetadataServiceTest {
|
||||
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"),
|
||||
@ -58,13 +62,15 @@ final class AssetStudioMetadataServiceTest {
|
||||
Files.writeString(assetRoot.resolve(AssetStudioMetadataService.SCENE_BANK_SUPPORT_FILE), """
|
||||
{
|
||||
"schema_version": 1,
|
||||
"map_width": 16,
|
||||
"map_height": 16,
|
||||
"layer_count": 5,
|
||||
"layers": [
|
||||
{ "index": 1, "tilemap": "a.tmx" },
|
||||
{ "index": 2, "tilemap": "b.tmx" },
|
||||
{ "index": 3, "tilemap": "c.tmx" },
|
||||
{ "index": 4, "tilemap": "d.tmx" },
|
||||
{ "index": 5, "tilemap": "e.tmx" }
|
||||
{ "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": "" }
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
@ -0,0 +1,141 @@
|
||||
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,
|
||||
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")),
|
||||
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));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user