From 4e7eba20ac79006112a9b47eb7c452400bb36ef2 Mon Sep 17 00:00:00 2001 From: leo <80651389+civilwarinfrance@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:51:48 -0700 Subject: [PATCH 1/2] Load template tile objects without preexisting map tilesets AI-assisted by Codex (GPT-6). --- pytiled_parser/parsers/json/tiled_map.py | 10 +++- pytiled_parser/parsers/tmx/tiled_map.py | 10 +++- tests/test_template_tilesets.py | 73 ++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 tests/test_template_tilesets.py diff --git a/pytiled_parser/parsers/json/tiled_map.py b/pytiled_parser/parsers/json/tiled_map.py index 299b9193..09f8871f 100644 --- a/pytiled_parser/parsers/json/tiled_map.py +++ b/pytiled_parser/parsers/json/tiled_map.py @@ -156,9 +156,13 @@ def parse(file: Path, encoding: str) -> TiledMap: break if not already_loaded: - highest_firstgid = max(map_.tilesets.keys()) - last_tileset_count = map_.tilesets[highest_firstgid].tile_count - new_firstgid = highest_firstgid + last_tileset_count + new_firstgid = 1 + if map_.tilesets: + highest_firstgid = max(map_.tilesets.keys()) + last_tileset_count = map_.tilesets[ + highest_firstgid + ].tile_count + new_firstgid = highest_firstgid + last_tileset_count map_.tilesets[new_firstgid] = parse_json_tileset( tiled_object.new_tileset, new_firstgid, diff --git a/pytiled_parser/parsers/tmx/tiled_map.py b/pytiled_parser/parsers/tmx/tiled_map.py index 425ee13b..0db899e2 100644 --- a/pytiled_parser/parsers/tmx/tiled_map.py +++ b/pytiled_parser/parsers/tmx/tiled_map.py @@ -110,9 +110,13 @@ def parse(file: Path, encoding: str) -> TiledMap: if not already_loaded: print("here") - highest_firstgid = max(map_.tilesets.keys()) - last_tileset_count = map_.tilesets[highest_firstgid].tile_count - new_firstgid = highest_firstgid + last_tileset_count + new_firstgid = 1 + if map_.tilesets: + highest_firstgid = max(map_.tilesets.keys()) + last_tileset_count = map_.tilesets[ + highest_firstgid + ].tile_count + new_firstgid = highest_firstgid + last_tileset_count map_.tilesets[new_firstgid] = parse_tmx_tileset( tiled_object.new_tileset, new_firstgid, diff --git a/tests/test_template_tilesets.py b/tests/test_template_tilesets.py new file mode 100644 index 00000000..6dbda5f6 --- /dev/null +++ b/tests/test_template_tilesets.py @@ -0,0 +1,73 @@ +"""Tilesets supplied by object templates must also work without map tilesets.""" + +import copy +import json +import xml.etree.ElementTree as etree +from pathlib import Path + +import pytest + +from pytiled_parser import parse_map + +TEMPLATE_DATA = Path(__file__).parent / "test_data" / "map_tests" / "template" + + +@pytest.mark.parametrize("format_", ["json", "tmx"]) +@pytest.mark.parametrize("with_map_tilesets", [False, True]) +def test_template_tileset_allocation(tmp_path, format_, with_map_tilesets): + source = TEMPLATE_DATA / ("map." + format_) + destination = tmp_path / source.name + if format_ == "json": + document = json.loads(source.read_text()) + if with_map_tilesets: + for tileset in document["tilesets"]: + tileset["source"] = str((TEMPLATE_DATA / tileset["source"]).resolve()) + else: + document["tilesets"] = [] + document["nextobjectid"] = 9 + objects = document["layers"][0]["objects"] + objects.append(copy.deepcopy(objects[1])) + objects[-1]["id"] = 8 + for object_ in objects: + object_["template"] = str((TEMPLATE_DATA / object_["template"]).resolve()) + destination.write_text(json.dumps(document)) + else: + document = etree.parse(source) + for tileset in document.findall("tileset"): + if with_map_tilesets: + tileset.set( + "source", str((TEMPLATE_DATA / tileset.get("source")).resolve()) + ) + else: + document.getroot().remove(tileset) + document.getroot().set("nextobjectid", "9") + layer = document.find("objectgroup") + repeated = copy.deepcopy(layer.findall("object")[1]) + repeated.set("id", "8") + layer.append(repeated) + for object_ in layer.findall("object"): + object_.set( + "template", str((TEMPLATE_DATA / object_.get("template")).resolve()) + ) + document.write(destination, encoding="utf-8") + + parsed = parse_map(destination) + objects = parsed.layers[0].tiled_objects + expected_gids = [49, 50, 49] if with_map_tilesets else [1, 2, 1] + assert [object_.gid for object_ in objects[1:]] == expected_gids + assert sorted(parsed.tilesets) == ([1, 49, 50] if with_map_tilesets else [1, 2]) + for gid, name in zip( + expected_gids, + [ + "tile_set_image_for_template", + "tile_set_single_image", + "tile_set_image_for_template", + ], + ): + assert parsed.tilesets[gid].name == name + assert parsed.tilesets[gid].firstgid == gid + assert ( + parsed.tilesets[gid].image is not None + or parsed.tilesets[gid].tiles[0].image is not None + ) + assert all(object_.new_tileset is None for object_ in objects[1:]) From 05a078640fd09fe2791303e4bb9c2e91eff0f68d Mon Sep 17 00:00:00 2001 From: leo <80651389+civilwarinfrance@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:04:39 -0700 Subject: [PATCH 2/2] Resolve template tilesets inside nested layer groups AI-assisted by Codex (GPT-6). --- pytiled_parser/parsers/json/tiled_map.py | 8 ++++++- pytiled_parser/parsers/tmx/tiled_map.py | 8 ++++++- tests/test_template_tilesets.py | 29 ++++++++++++++++++++++-- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/pytiled_parser/parsers/json/tiled_map.py b/pytiled_parser/parsers/json/tiled_map.py index 09f8871f..dbec39d1 100644 --- a/pytiled_parser/parsers/json/tiled_map.py +++ b/pytiled_parser/parsers/json/tiled_map.py @@ -7,6 +7,7 @@ from pytiled_parser.common_types import OrderedPair, Size from pytiled_parser.exception import UnknownFormat +from pytiled_parser.layer import LayerGroup from pytiled_parser.parsers.json.layer import RawLayer from pytiled_parser.parsers.json.layer import parse as parse_layer from pytiled_parser.parsers.json.properties import RawProperty @@ -130,9 +131,14 @@ def parse(file: Path, encoding: str) -> TiledMap: version=version, ) - layers = [layer for layer in map_.layers if hasattr(layer, "tiled_objects")] + layers = list(map_.layers) for my_layer in layers: + if isinstance(my_layer, LayerGroup): + layers.extend(my_layer.layers or []) + continue + if not hasattr(my_layer, "tiled_objects"): + continue # Mypy extremely hates what is going on in this whole block # For some reason an ignore on this first for loop is causing it # to just not care about any of the problems in here. diff --git a/pytiled_parser/parsers/tmx/tiled_map.py b/pytiled_parser/parsers/tmx/tiled_map.py index 0db899e2..bdf15d2e 100644 --- a/pytiled_parser/parsers/tmx/tiled_map.py +++ b/pytiled_parser/parsers/tmx/tiled_map.py @@ -4,6 +4,7 @@ from pytiled_parser.common_types import OrderedPair, Size from pytiled_parser.exception import UnknownFormat +from pytiled_parser.layer import LayerGroup from pytiled_parser.parsers.json.tileset import parse as parse_json_tileset from pytiled_parser.parsers.tmx.layer import parse as parse_layer from pytiled_parser.parsers.tmx.properties import parse as parse_properties @@ -83,9 +84,14 @@ def parse(file: Path, encoding: str) -> TiledMap: version=raw_map.attrib["version"], ) - layers = [layer for layer in map_.layers if hasattr(layer, "tiled_objects")] + layers = list(map_.layers) for my_layer in layers: + if isinstance(my_layer, LayerGroup): + layers.extend(my_layer.layers or []) + continue + if not hasattr(my_layer, "tiled_objects"): + continue # Mypy extremely hates what is going on in this whole block # For some reason an ignore on this first for loop is causing it # to just not care about any of the problems in here. diff --git a/tests/test_template_tilesets.py b/tests/test_template_tilesets.py index 6dbda5f6..ba787b59 100644 --- a/tests/test_template_tilesets.py +++ b/tests/test_template_tilesets.py @@ -12,9 +12,10 @@ TEMPLATE_DATA = Path(__file__).parent / "test_data" / "map_tests" / "template" +@pytest.mark.parametrize("group_depth", [0, 1, 3]) @pytest.mark.parametrize("format_", ["json", "tmx"]) @pytest.mark.parametrize("with_map_tilesets", [False, True]) -def test_template_tileset_allocation(tmp_path, format_, with_map_tilesets): +def test_template_tileset_allocation(tmp_path, format_, with_map_tilesets, group_depth): source = TEMPLATE_DATA / ("map." + format_) destination = tmp_path / source.name if format_ == "json": @@ -30,6 +31,20 @@ def test_template_tileset_allocation(tmp_path, format_, with_map_tilesets): objects[-1]["id"] = 8 for object_ in objects: object_["template"] = str((TEMPLATE_DATA / object_["template"]).resolve()) + for depth in range(group_depth): + document["layers"] = [ + { + "id": 3 + depth, + "name": "group", + "type": "group", + "opacity": 1, + "visible": True, + "x": 0, + "y": 0, + "layers": document["layers"], + } + ] + document["nextlayerid"] = 3 + group_depth destination.write_text(json.dumps(document)) else: document = etree.parse(source) @@ -49,10 +64,20 @@ def test_template_tileset_allocation(tmp_path, format_, with_map_tilesets): object_.set( "template", str((TEMPLATE_DATA / object_.get("template")).resolve()) ) + document.getroot().remove(layer) + for depth in range(group_depth): + group = etree.Element("group", id=str(3 + depth), name="group") + group.append(layer) + layer = group + document.getroot().append(layer) + document.getroot().set("nextlayerid", str(3 + group_depth)) document.write(destination, encoding="utf-8") parsed = parse_map(destination) - objects = parsed.layers[0].tiled_objects + layer = parsed.layers[0] + for _ in range(group_depth): + layer = layer.layers[0] + objects = layer.tiled_objects expected_gids = [49, 50, 49] if with_map_tilesets else [1, 2, 1] assert [object_.gid for object_ in objects[1:]] == expected_gids assert sorted(parsed.tilesets) == ([1, 49, 50] if with_map_tilesets else [1, 2])