implements PLN-0054

This commit is contained in:
bQUARKz 2026-04-18 17:54:00 +01:00
parent bcc5b413fa
commit 33fd7c485e
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
24 changed files with 1117 additions and 13 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
package p.studio.workspaces.assets.tiled;
public record TiledPoint(double x, double y) {
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
package p.studio.workspaces.assets.tiled;
public final class TiledUnsupportedFeatureException extends RuntimeException {
public TiledUnsupportedFeatureException(String message) {
super(message);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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