diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 5faeb7ca..4f0655df 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -9,12 +9,14 @@ jobs: name: Build and Publish to PyPI runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags') + permissions: + id-token: write steps: - uses: actions/checkout@master - - name: Set up Python 3.10 - uses: actions/setup-python@v3 + - name: Set up Python 3.13 + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.13" - name: Install pypa/build run: >- python -m pip install --user ".[build]" @@ -23,5 +25,3 @@ jobs: python -m build --sdist --wheel --outdir dist/ - name: Publish Distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d780ea28..91bd5ce2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] architecture: ['x64'] steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 61db67dd..a6c4f148 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,40 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +## [2.2.9] - 2025-01-23 + +Fixes a bug where object templates would cause an error when used inside of TileSet. This occurs when using an object template to define collision details on a tile within a tileset. See #82. + +## [2.2.8] - 2025-01-16 + +Add explicit support for 3.13, previous versions work on 3.13 but it was not explicitly labeled as supported or being tested by CI. + +Converts all file loading to use UTF-8 encoding by default. In most cases, all Tiled files will be exported from Tiled in UTF-8 encoding, however the python `open()` function uses the system default locale. The only case where Tiled would not have used UTF-8 is for JSON files when Tiled was compiled against Qt 5, which is only in some builds of Tiled from older systems. All XML files exported from Tiled will always be UTF-8. If someone happens to have a JSON file which was exported from Tiled on an encoding other than UTF-8, or for some other reason is in a different encoding. This can be switched using a new optional argument named `encoding` in the various public API `parse` functions such as `parse_map()`. This value is handed down through the pipeline of file loading in pytiled-parser, and will apply to every file loaded during the chain from this. This means that every file in a chain(for example, Map, Tileset, and Template File) must share the same encoding. This new argument is a string which is ultimately passed to the Python [open()](https://docs.python.org/3/library/functions.html#open) function. This change does introduce breaking changes in the underlying API which is not intended to be public facing, but if you are going deeper than the top level parse functions, you may need to adjust for this, as many of the underlying internal functions now have a mandatory encoding argument. + +Property values which are of type "int" in Tiled will now be loaded as Python ints. Previously these values were loaded as floats, which for most purposes is fine, but not exactly correct. If a float value happens to find it's way into an int type property, pytiled-parser will mimic Tiled's functionality and round it up/down to the nearest integer value. This may technically be a breaking change for some obscure runtime type checking use cases, but shouldn't really break too much. + +Previously when loading object templates, if the template had a `name` key defined, it would override the object instance `name` value. This has been changed such that the object instance `name` will be respected, and override any name provided by a template. This is in line with Tiled, and it does not appear to actually be possible to provide a `name` value in an object template, but you can technically put the field into the template file manually, and with JSON, Tiled actually stores an empty string in the field(as opposed to it just not existing in the TMX file), so with JSON the fields were being overwritten to an empty string. + +Previously with the TMX format, the order of layers within a Group Layer were not guaranteed, this has been fixed and all layers and group layers should be in their proper order. + +## [2.2.7] - 2024-10-03 + +Fixes a bug when using the TMX format, where multi-line String properties would not be correctly parsed, as they are placed differently in the XML than single line strings. (#75) + +The Tiled docs also state that this multi-line format may in the future be used for all property values, so this change will help to futureproof against that. + +## [2.2.6] - 2024-08-22 + +Fixes a bug where properties did not load as expected on objects when using object templates. As of this release, the functionality is such that if properties are defined on both an object, and it's template, they will both end up on the resulting object, with the ones defined directly on the object overriding any properties that have the same name from the template. It does not compare types, so a String property with the name `test` would override a number property with the name `test`, as an example. Comparing types could be done in the future, but is likely more complicated than it's worth doing right now. + +Fixes a bug where the TMX parser would report all layers as top level layers, ignoring the layer group nesting. This bug was not present in the JSON parser. (#74) + +Also handles some small deprecation warnings related to true/false comparisons of etree.Element classes in the TMX parser. + +## [2.2.5] - 2024-07-01 + +Adds a `__all__` section to the main library `__init__.py` file which fixes problems when running pyright in strict mode against this library, it would not be able to see the exported types. + ## [2.2.4] - 2024-07-01 Small change to the default text color, in Tiled the text color defaults to blac(0, 0, 0), previously in pytiled-parser if the color was not specified it would default to white(255, 255, 255). This has been changed to match Tiled's behavior [#70](https://github.com/pythonarcade/pytiled_parser/pull/70) diff --git a/README.md b/README.md index 3d559f07..faba7630 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ The [Arcade](https://api.arcade.academy) library has [supporting code](https://api.arcade.academy/en/latest/api/tilemap.html) to integrate PyTiled Parser and [example code](https://api.arcade.academy/en/latest/examples/index.html#using-tiled-map-editor-to-create-maps) showing its use. +## Supported Python Versions + +pytiled-parser works on Python 3.6 - 3.13(and should continue to work on new versions). However we are not actively testing on Python 3.6 and 3.7 due to other development tooling not supporting them, however as long as it remains tenable to do so, we will support these versions and fix problems for them. + ## Installation Simply install with pip: diff --git a/pyproject.toml b/pyproject.toml index 8a748020..249e89fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,45 +1,39 @@ [project] name = "pytiled_parser" -version = "2.2.4" +version = "2.2.9" description = "A library for parsing Tiled Map Editor maps and tilesets" readme = "README.md" authors = [ - {name="Benjamin Kirkbride", email="BenjaminKirkbride@gmail.com"}, - {name="Darren Eberly", email="Darren.Eberly@gmail.com"}, + { name = "Benjamin Kirkbride", email = "BenjaminKirkbride@gmail.com" }, + { name = "Darren Eberly", email = "Darren.Eberly@gmail.com" }, ] -maintainers = [ - {name="Darren Eberly", email="Darren.Eberly@gmail.com"} -] -license = {file = "LICENSE"} +maintainers = [{ name = "Darren Eberly", email = "Darren.Eberly@gmail.com" }] +license = { file = "LICENSE" } requires-python = ">=3.6" classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: Implementation :: CPython", - "Topic :: Software Development :: Libraries :: Python Modules" -] -dependencies = [ - "attrs >= 18.2.0", - "typing-extensions" + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Software Development :: Libraries :: Python Modules", ] +dependencies = ["attrs >= 18.2.0", "typing-extensions"] [project.urls] homepage = "https://github.com/pythonarcade/pytiled_parser" [project.optional-dependencies] -zstd = [ - "zstd" -] +zstd = ["zstd"] dev = [ "pytest", @@ -50,20 +44,12 @@ dev = [ "sphinx", "sphinx-sitemap", "myst-parser", - "furo" + "furo", ] -tests = [ - "pytest", - "pytest-cov", - "black", - "ruff", - "mypy" -] +tests = ["pytest", "pytest-cov", "black", "ruff", "mypy"] -build = [ - "build" -] +build = ["build"] [tool.setuptools.packages.find] include = ["pytiled_parser", "pytiled_parser.*"] @@ -82,7 +68,7 @@ branch = true show_missing = true [tool.mypy] -python_version = 3.11 +python_version = "3.13" warn_unused_configs = true warn_redundant_casts = true ignore_missing_imports = true @@ -93,4 +79,4 @@ ignore_errors = true [tool.ruff] exclude = ["__init__.py"] -ignore = ["E501"] \ No newline at end of file +ignore = ["E501"] diff --git a/pytiled_parser/__init__.py b/pytiled_parser/__init__.py index b52e8fbb..ad097f39 100644 --- a/pytiled_parser/__init__.py +++ b/pytiled_parser/__init__.py @@ -19,3 +19,29 @@ from .tiled_map import TiledMap from .tileset import Frame, Grid, Tile, Tileset, Transformations from .world import World, WorldMap + +__all__ = [ + "Color", + "OrderedPair", + "Size", + "UnknownFormat", + "Chunk", + "ImageLayer", + "Layer", + "LayerGroup", + "ObjectLayer", + "TileLayer", + "parse_map", + "parse_world", + "parse_tileset", + "Properties", + "Property", + "TiledMap", + "Frame", + "Grid", + "Tile", + "Tileset", + "Transformations", + "World", + "WorldMap", +] diff --git a/pytiled_parser/parser.py b/pytiled_parser/parser.py index 22918ecc..c7cb97d5 100644 --- a/pytiled_parser/parser.py +++ b/pytiled_parser/parser.py @@ -14,23 +14,24 @@ from pytiled_parser.world import parse_world as _parse_world -def parse_map(file: Path) -> TiledMap: +def parse_map(file: Path, encoding: str = "utf-8") -> TiledMap: """Parse the raw Tiled map into a pytiled_parser type Args: file: Path to the map file + encoding: The character encoding set to use when opening the file Returns: TiledMap: A parsed and typed TiledMap """ - parser = check_format(file) + parser = check_format(file, encoding) # The type ignores are because mypy for some reason thinks those functions return Any if parser == "tmx": - return tmx_map_parse(file) # type: ignore + return tmx_map_parse(file, encoding) # type: ignore else: try: - return json_map_parse(file) # type: ignore + return json_map_parse(file, encoding) # type: ignore except ValueError: raise UnknownFormat( "Unknown Map Format, please use either the TMX or JSON format. " @@ -38,26 +39,27 @@ def parse_map(file: Path) -> TiledMap: ) -def parse_tileset(file: Path) -> Tileset: +def parse_tileset(file: Path, encoding: str = "utf-8") -> Tileset: """Parse the raw Tiled Tileset into a pytiled_parser type Args: file: Path to the map file + encoding: The character encoding set to use when opening the file Returns: Tileset: A parsed and typed Tileset """ - parser = check_format(file) + parser = check_format(file, encoding) if parser == "tmx": - with open(file) as map_file: + with open(file, encoding=encoding) as map_file: raw_tileset = etree.parse(map_file).getroot() - return tmx_tileset_parse(raw_tileset, 1) + return tmx_tileset_parse(raw_tileset, 1, encoding) else: try: - with open(file) as my_file: + with open(file, encoding=encoding) as my_file: raw_tileset = json.load(my_file) - return json_tileset_parse(raw_tileset, 1) + return json_tileset_parse(raw_tileset, 1, encoding) except ValueError: raise UnknownFormat( "Unknowm Tileset Format, please use either the TSX or JSON format. " @@ -65,13 +67,14 @@ def parse_tileset(file: Path) -> Tileset: ) -def parse_world(file: Path) -> World: +def parse_world(file: Path, encoding: str = "utf-8") -> World: """Parse the raw world file into a pytiled_parser type Args: file: Path to the world file + encoding: The character encoding set to use when opening the file Returns: World: A parsed and typed World """ - return _parse_world(file) + return _parse_world(file, encoding) diff --git a/pytiled_parser/parsers/json/layer.py b/pytiled_parser/parsers/json/layer.py index 9682fc4e..d4ae6e69 100644 --- a/pytiled_parser/parsers/json/layer.py +++ b/pytiled_parser/parsers/json/layer.py @@ -298,6 +298,7 @@ def _parse_tile_layer(raw_layer: RawLayer) -> TileLayer: def _parse_object_layer( raw_layer: RawLayer, + encoding: str, parent_dir: Optional[Path] = None, ) -> ObjectLayer: """Parse the raw_layer to an ObjectLayer. @@ -310,7 +311,7 @@ def _parse_object_layer( """ objects = [] for object_ in raw_layer["objects"]: - objects.append(parse_object(object_, parent_dir)) + objects.append(parse_object(object_, encoding, parent_dir)) return ObjectLayer( tiled_objects=objects, @@ -339,7 +340,7 @@ def _parse_image_layer(raw_layer: RawLayer) -> ImageLayer: def _parse_group_layer( - raw_layer: RawLayer, parent_dir: Optional[Path] = None + raw_layer: RawLayer, encoding, parent_dir: Optional[Path] = None ) -> LayerGroup: """Parse the raw_layer to a LayerGroup. @@ -352,13 +353,14 @@ def _parse_group_layer( layers = [] for layer in raw_layer["layers"]: - layers.append(parse(layer, parent_dir=parent_dir)) + layers.append(parse(layer, encoding, parent_dir=parent_dir)) return LayerGroup(layers=layers, **_parse_common(raw_layer).__dict__) def parse( raw_layer: RawLayer, + encoding: str, parent_dir: Optional[Path] = None, ) -> Layer: """Parse a raw Layer into a pytiled_parser object. @@ -378,9 +380,9 @@ def parse( type_ = raw_layer["type"] if type_ == "objectgroup": - return _parse_object_layer(raw_layer, parent_dir) + return _parse_object_layer(raw_layer, encoding, parent_dir) elif type_ == "group": - return _parse_group_layer(raw_layer, parent_dir) + return _parse_group_layer(raw_layer, encoding, parent_dir) elif type_ == "imagelayer": return _parse_image_layer(raw_layer) elif type_ == "tilelayer": diff --git a/pytiled_parser/parsers/json/properties.py b/pytiled_parser/parsers/json/properties.py index 62b2e17b..8615619a 100644 --- a/pytiled_parser/parsers/json/properties.py +++ b/pytiled_parser/parsers/json/properties.py @@ -45,6 +45,8 @@ def parse(raw_properties: List[RawProperty]) -> Properties: value = Path(cast(str, raw_property["value"])) elif raw_property["type"] == "color": value = parse_color(cast(str, raw_property["value"])) + elif raw_property["type"] == "int": + value = round(cast(float, raw_property["value"])) else: value = raw_property["value"] final[raw_property["name"]] = value diff --git a/pytiled_parser/parsers/json/tiled_map.py b/pytiled_parser/parsers/json/tiled_map.py index a5113f54..299b9193 100644 --- a/pytiled_parser/parsers/json/tiled_map.py +++ b/pytiled_parser/parsers/json/tiled_map.py @@ -55,7 +55,7 @@ """ -def parse(file: Path) -> TiledMap: +def parse(file: Path, encoding: str) -> TiledMap: """Parse the raw Tiled map into a pytiled_parser type. Args: @@ -64,7 +64,7 @@ def parse(file: Path) -> TiledMap: Returns: TiledMap: A parsed TiledMap. """ - with open(file) as map_file: + with open(file, encoding=encoding) as map_file: raw_tiled_map = json.load(map_file) parent_dir = file.parent @@ -76,13 +76,14 @@ def parse(file: Path) -> TiledMap: if raw_tileset.get("source") is not None: # Is an external Tileset tileset_path = Path(parent_dir / raw_tileset["source"]) - parser = check_format(tileset_path) - with open(tileset_path) as raw_tileset_file: + parser = check_format(tileset_path, encoding) + with open(tileset_path, encoding=encoding) as raw_tileset_file: if parser == "tmx": raw_tileset_external = etree.parse(raw_tileset_file).getroot() tilesets[raw_tileset["firstgid"]] = parse_tmx_tileset( raw_tileset_external, raw_tileset["firstgid"], + encoding, external_path=tileset_path.parent, ) else: @@ -90,6 +91,7 @@ def parse(file: Path) -> TiledMap: tilesets[raw_tileset["firstgid"]] = parse_json_tileset( json.load(raw_tileset_file), raw_tileset["firstgid"], + encoding, external_path=tileset_path.parent, ) except ValueError: @@ -101,7 +103,7 @@ def parse(file: Path) -> TiledMap: # Is an embedded Tileset raw_tileset = cast(RawTileSet, raw_tileset) tilesets[raw_tileset["firstgid"]] = parse_json_tileset( - raw_tileset, raw_tileset["firstgid"] + raw_tileset, raw_tileset["firstgid"], encoding ) if isinstance(raw_tiled_map["version"], float): # pragma: no cover @@ -113,7 +115,10 @@ def parse(file: Path) -> TiledMap: map_ = TiledMap( map_file=file, infinite=raw_tiled_map.get("infinite", False), - layers=[parse_layer(layer_, parent_dir) for layer_ in raw_tiled_map["layers"]], + layers=[ + parse_layer(layer_, encoding, parent_dir) + for layer_ in raw_tiled_map["layers"] + ], map_size=Size(raw_tiled_map["width"], raw_tiled_map["height"]), next_layer_id=raw_tiled_map.get("nextlayerid"), next_object_id=raw_tiled_map["nextobjectid"], @@ -157,6 +162,7 @@ def parse(file: Path) -> TiledMap: map_.tilesets[new_firstgid] = parse_json_tileset( tiled_object.new_tileset, new_firstgid, + encoding, tiled_object.new_tileset_path, ) tiled_object.gid = tiled_object.gid + (new_firstgid - 1) diff --git a/pytiled_parser/parsers/json/tiled_object.py b/pytiled_parser/parsers/json/tiled_object.py index d5f3f08a..6ca3dabb 100644 --- a/pytiled_parser/parsers/json/tiled_object.py +++ b/pytiled_parser/parsers/json/tiled_object.py @@ -291,6 +291,7 @@ def _get_parser(raw_object: RawObject) -> Callable[[RawObject], TiledObject]: def parse( raw_object: RawObject, + encoding: str, parent_dir: Optional[Path] = None, ) -> TiledObject: """Parse the raw object into a pytiled_parser version @@ -314,13 +315,33 @@ def parse( "A parent directory must be specified when using object templates." ) template_path = Path(parent_dir / raw_object["template"]) - template, new_tileset, new_tileset_path = load_object_template(template_path) + template, new_tileset, new_tileset_path = load_object_template( + template_path, encoding + ) if isinstance(template, dict): loaded_template = template["object"] for key in loaded_template: if key != "id": - raw_object[key] = loaded_template[key] # type: ignore + if key == "properties": + if "properties" not in raw_object: + raw_object["properties"] = [] + + for prop in loaded_template["properties"]: + + found = False + for prop2 in raw_object["properties"]: + if prop2["name"] == prop["name"]: + found = True + break + + if not found: + raw_object["properties"].append(prop) + elif key == "name": + if "name" not in raw_object: + raw_object["name"] = loaded_template[key] + else: + raw_object[key] = loaded_template[key] # type: ignore else: raise NotImplementedError( "Loading TMX object templates inside a JSON map is currently not supported, " diff --git a/pytiled_parser/parsers/json/tileset.py b/pytiled_parser/parsers/json/tileset.py index cbbc51de..c7ad61aa 100644 --- a/pytiled_parser/parsers/json/tileset.py +++ b/pytiled_parser/parsers/json/tileset.py @@ -161,7 +161,9 @@ def _parse_grid(raw_grid: RawGrid) -> Grid: ) -def _parse_tile(raw_tile: RawTile, external_path: Optional[Path] = None) -> Tile: +def _parse_tile( + raw_tile: RawTile, encoding: str, external_path: Optional[Path] = None +) -> Tile: """Parse the raw_tile to a Tile object. Args: @@ -180,7 +182,7 @@ def _parse_tile(raw_tile: RawTile, external_path: Optional[Path] = None) -> Tile tile.animation.append(_parse_frame(frame)) if raw_tile.get("objectgroup") is not None: - tile.objects = parse_layer(raw_tile["objectgroup"]) + tile.objects = parse_layer(raw_tile["objectgroup"], encoding, external_path) if raw_tile.get("properties") is not None: tile.properties = parse_properties(raw_tile["properties"]) @@ -231,6 +233,7 @@ def _parse_tile(raw_tile: RawTile, external_path: Optional[Path] = None) -> Tile def parse( raw_tileset: RawTileSet, firstgid: int, + encoding: str, external_path: Optional[Path] = None, ) -> Tileset: """Parse the raw tileset into a pytiled_parser type @@ -309,12 +312,12 @@ def parse( assert raw_tile.get("id") is None raw_tile["id"] = int(raw_tile_id) tiles[raw_tile["id"]] = _parse_tile( - raw_tile, external_path=external_path + raw_tile, encoding, external_path=external_path ) else: for raw_tile in raw_tileset["tiles"]: tiles[raw_tile["id"]] = _parse_tile( - raw_tile, external_path=external_path + raw_tile, encoding, external_path=external_path ) tileset.tiles = tiles diff --git a/pytiled_parser/parsers/tmx/layer.py b/pytiled_parser/parsers/tmx/layer.py index ffb8e41e..b4ebc17d 100644 --- a/pytiled_parser/parsers/tmx/layer.py +++ b/pytiled_parser/parsers/tmx/layer.py @@ -262,7 +262,7 @@ def _parse_tile_layer(raw_layer: etree.Element) -> TileLayer: def _parse_object_layer( - raw_layer: etree.Element, parent_dir: Optional[Path] = None + raw_layer: etree.Element, encoding: str, parent_dir: Optional[Path] = None ) -> ObjectLayer: """Parse the raw_layer to an ObjectLayer. @@ -274,7 +274,7 @@ def _parse_object_layer( """ objects = [] for object_ in raw_layer.findall("./object"): - objects.append(parse_object(object_, parent_dir)) + objects.append(parse_object(object_, encoding, parent_dir)) object_layer = ObjectLayer( tiled_objects=objects, @@ -316,7 +316,7 @@ def _parse_image_layer(raw_layer: etree.Element) -> ImageLayer: def _parse_group_layer( - raw_layer: etree.Element, parent_dir: Optional[Path] = None + raw_layer: etree.Element, encoding: str, parent_dir: Optional[Path] = None ) -> LayerGroup: """Parse the raw_layer to a LayerGroup. @@ -327,29 +327,16 @@ def _parse_group_layer( LayerGroup: The LayerGroup created from raw_layer """ layers: List[Layer] = [] - for layer in raw_layer.findall("./layer"): - layers.append(_parse_tile_layer(layer)) - - for layer in raw_layer.findall("./objectgroup"): - layers.append(_parse_object_layer(layer, parent_dir)) - - for layer in raw_layer.findall("./imagelayer"): - layers.append(_parse_image_layer(layer)) - - for layer in raw_layer.findall("./group"): - layers.append(_parse_group_layer(layer, parent_dir)) - # layers = [] - # layers = [ - # parse(child_layer, parent_dir=parent_dir) - # for child_layer in raw_layer.iter() - # if child_layer.tag in ["layer", "objectgroup", "imagelayer", "group"] - # ] + for element in raw_layer: + if element.tag in ["layer", "objectgroup", "imagelayer", "group"]: + layers.append(parse(element, encoding, parent_dir)) return LayerGroup(layers=layers, **_parse_common(raw_layer).__dict__) def parse( raw_layer: etree.Element, + encoding: str, parent_dir: Optional[Path] = None, ) -> Layer: """Parse a raw Layer into a pytiled_parser object. @@ -369,9 +356,9 @@ def parse( type_ = raw_layer.tag if type_ == "objectgroup": - return _parse_object_layer(raw_layer, parent_dir) + return _parse_object_layer(raw_layer, encoding, parent_dir) elif type_ == "group": - return _parse_group_layer(raw_layer, parent_dir) + return _parse_group_layer(raw_layer, encoding, parent_dir) elif type_ == "imagelayer": return _parse_image_layer(raw_layer) elif type_ == "layer": diff --git a/pytiled_parser/parsers/tmx/properties.py b/pytiled_parser/parsers/tmx/properties.py index 1b20e3d4..57731d25 100644 --- a/pytiled_parser/parsers/tmx/properties.py +++ b/pytiled_parser/parsers/tmx/properties.py @@ -1,7 +1,7 @@ import xml.etree.ElementTree as etree from pathlib import Path -from pytiled_parser.properties import Properties, Property +from pytiled_parser.properties import Properties, Property, ClassProperty from pytiled_parser.util import parse_color @@ -11,17 +11,24 @@ def parse(raw_properties: etree.Element) -> Properties: for raw_property in raw_properties.findall("property"): type_ = raw_property.attrib.get("type") - - if "value" not in raw_property.attrib: + if type_ == "class": + children_nodes = raw_property.find("./properties") + x = ClassProperty( + raw_property.attrib["propertytype"], parse(children_nodes) if children_nodes is not None else {}) + final[raw_property.attrib["name"]] = x continue - value_ = raw_property.attrib["value"] + value_ = raw_property.attrib.get("value", raw_property.text) + if value_ is None: + continue if type_ == "file": value = Path(value_) elif type_ == "color": value = parse_color(value_) - elif type_ == "int" or type_ == "float": + elif type_ == "int": + value = round(float(value_)) + elif type_ == "float": value = float(value_) elif type_ == "bool": if value_ == "true": diff --git a/pytiled_parser/parsers/tmx/tiled_map.py b/pytiled_parser/parsers/tmx/tiled_map.py index 988e5cfa..425ee13b 100644 --- a/pytiled_parser/parsers/tmx/tiled_map.py +++ b/pytiled_parser/parsers/tmx/tiled_map.py @@ -12,7 +12,7 @@ from pytiled_parser.util import check_format, parse_color -def parse(file: Path) -> TiledMap: +def parse(file: Path, encoding: str) -> TiledMap: """Parse the raw Tiled map into a pytiled_parser type. Args: @@ -21,7 +21,7 @@ def parse(file: Path) -> TiledMap: Returns: TiledMap: A parsed TiledMap. """ - with open(file) as map_file: + with open(file, encoding=encoding) as map_file: raw_map = etree.parse(map_file).getroot() parent_dir = file.parent @@ -33,19 +33,21 @@ def parse(file: Path) -> TiledMap: if raw_tileset.attrib.get("source") is not None: # Is an external Tileset tileset_path = Path(parent_dir / raw_tileset.attrib["source"]) - parser = check_format(tileset_path) - with open(tileset_path) as tileset_file: + parser = check_format(tileset_path, encoding) + with open(tileset_path, encoding=encoding) as tileset_file: if parser == "tmx": raw_tileset_external = etree.parse(tileset_file).getroot() tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tmx_tileset( raw_tileset_external, int(raw_tileset.attrib["firstgid"]), + encoding, external_path=tileset_path.parent, ) elif parser == "json": tilesets[int(raw_tileset.attrib["firstgid"])] = parse_json_tileset( json.load(tileset_file), int(raw_tileset.attrib["firstgid"]), + encoding, external_path=tileset_path.parent, ) else: @@ -56,13 +58,13 @@ def parse(file: Path) -> TiledMap: else: # Is an embedded Tileset tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tmx_tileset( - raw_tileset, int(raw_tileset.attrib["firstgid"]) + raw_tileset, int(raw_tileset.attrib["firstgid"]), encoding ) layers = [] - for element in raw_map.iter(): + for element in raw_map: if element.tag in ["layer", "objectgroup", "imagelayer", "group"]: - layers.append(parse_layer(element, parent_dir)) + layers.append(parse_layer(element, encoding, parent_dir)) map_ = TiledMap( map_file=file, @@ -114,6 +116,7 @@ def parse(file: Path) -> TiledMap: map_.tilesets[new_firstgid] = parse_tmx_tileset( tiled_object.new_tileset, new_firstgid, + encoding, tiled_object.new_tileset_path, ) tiled_object.gid = tiled_object.gid + (new_firstgid - 1) @@ -133,7 +136,7 @@ def parse(file: Path) -> TiledMap: map_.hex_side_length = int(raw_map.attrib["hexsidelength"]) properties_element = raw_map.find("./properties") - if properties_element: + if properties_element is not None: map_.properties = parse_properties(properties_element) if raw_map.attrib.get("staggeraxis") is not None: diff --git a/pytiled_parser/parsers/tmx/tiled_object.py b/pytiled_parser/parsers/tmx/tiled_object.py index 711664e1..a36cfd1a 100644 --- a/pytiled_parser/parsers/tmx/tiled_object.py +++ b/pytiled_parser/parsers/tmx/tiled_object.py @@ -55,7 +55,7 @@ def _parse_common(raw_object: etree.Element) -> TiledObject: common.class_ = raw_object.attrib["class"] properties_element = raw_object.find("./properties") - if properties_element: + if properties_element is not None: common.properties = parse_properties(properties_element) return common @@ -245,7 +245,9 @@ def _get_parser(raw_object: etree.Element) -> Callable[[etree.Element], TiledObj return _parse_rectangle -def parse(raw_object: etree.Element, parent_dir: Optional[Path] = None) -> TiledObject: +def parse( + raw_object: etree.Element, encoding: str, parent_dir: Optional[Path] = None +) -> TiledObject: """Parse the raw object into a pytiled_parser version Args: @@ -267,7 +269,9 @@ def parse(raw_object: etree.Element, parent_dir: Optional[Path] = None) -> Tiled "A parent directory must be specified when using object templates." ) template_path = Path(parent_dir / raw_object.attrib["template"]) - template, new_tileset, new_tileset_path = load_object_template(template_path) + template, new_tileset, new_tileset_path = load_object_template( + template_path, encoding + ) if isinstance(template, etree.Element): new_object = template.find("./object") @@ -278,7 +282,26 @@ def parse(raw_object: etree.Element, parent_dir: Optional[Path] = None) -> Tiled new_object.attrib[key] = val properties_element = raw_object.find("./properties") - if properties_element is not None: + temp_properties_element = new_object.find("./properties") + if properties_element is not None and temp_properties_element is None: + new_object.append(properties_element) + elif properties_element is None and temp_properties_element is not None: + pass + elif ( + properties_element is not None + and temp_properties_element is not None + ): + for prop in temp_properties_element: + + found = False + for prop2 in properties_element: + if prop.attrib["name"] == prop2.attrib["name"]: + found = True + break + + if not found: + properties_element.append(prop) + new_object.remove(temp_properties_element) new_object.append(properties_element) raw_object = new_object diff --git a/pytiled_parser/parsers/tmx/tileset.py b/pytiled_parser/parsers/tmx/tileset.py index 5d788af8..9c34f975 100644 --- a/pytiled_parser/parsers/tmx/tileset.py +++ b/pytiled_parser/parsers/tmx/tileset.py @@ -63,7 +63,9 @@ def _parse_transformations(raw_transformations: etree.Element) -> Transformation ) -def _parse_tile(raw_tile: etree.Element, external_path: Optional[Path] = None) -> Tile: +def _parse_tile( + raw_tile: etree.Element, encoding: str, external_path: Optional[Path] = None +) -> Tile: """Parse the raw_tile to a Tile object. Args: @@ -89,7 +91,7 @@ def _parse_tile(raw_tile: etree.Element, external_path: Optional[Path] = None) - object_element = raw_tile.find("./objectgroup") if object_element is not None: - tile.objects = parse_layer(object_element) + tile.objects = parse_layer(object_element, encoding, external_path) properties_element = raw_tile.find("./properties") if properties_element is not None: @@ -129,6 +131,7 @@ def _parse_tile(raw_tile: etree.Element, external_path: Optional[Path] = None) - def parse( raw_tileset: etree.Element, firstgid: int, + encoding: str, external_path: Optional[Path] = None, ) -> Tileset: tileset = Tileset( @@ -204,7 +207,7 @@ def parse( tiles = {} for tile_element in raw_tileset.findall("./tile"): tiles[int(tile_element.attrib["id"])] = _parse_tile( - tile_element, external_path=external_path + tile_element, encoding, external_path=external_path ) if tiles: tileset.tiles = tiles diff --git a/pytiled_parser/parsers/tmx/wang_set.py b/pytiled_parser/parsers/tmx/wang_set.py index 0789cb10..34013117 100644 --- a/pytiled_parser/parsers/tmx/wang_set.py +++ b/pytiled_parser/parsers/tmx/wang_set.py @@ -38,7 +38,7 @@ def _parse_wang_color(raw_wang_color: etree.Element) -> WangColor: wang_color.class_ = raw_wang_color.attrib["class"] properties = raw_wang_color.find("./properties") - if properties: + if properties is not None: wang_color.properties = parse_properties(properties) return wang_color @@ -74,7 +74,7 @@ def parse(raw_wangset: etree.Element) -> WangSet: wangset.class_ = raw_wangset.attrib["class"] properties = raw_wangset.find("./properties") - if properties: + if properties is not None: wangset.properties = parse_properties(properties) return wangset diff --git a/pytiled_parser/properties.py b/pytiled_parser/properties.py index f8bc0acf..f9b94f71 100644 --- a/pytiled_parser/properties.py +++ b/pytiled_parser/properties.py @@ -13,6 +13,11 @@ from .common_types import Color -Property = Union[float, Path, str, bool, Color] +class ClassProperty(dict): + def __init__(self, propertytype:str, *args, **kwargs): + self.propertytype = propertytype or '' + dict.__init__(self, *args, **kwargs) + +Property = Union[int, float, Path, str, bool, Color, ClassProperty] Properties = Dict[str, Property] diff --git a/pytiled_parser/util.py b/pytiled_parser/util.py index 8ef4e257..089cdace 100644 --- a/pytiled_parser/util.py +++ b/pytiled_parser/util.py @@ -37,8 +37,8 @@ def parse_color(color: str) -> Color: raise ValueError("Improperly formatted color passed to parse_color") -def check_format(file_path: Path) -> str: - with open(file_path) as file: +def check_format(file_path: Path, encoding: str) -> str: + with open(file_path, encoding=encoding) as file: line = file.readline().rstrip().strip() if line[0] == "<": return "tmx" @@ -46,38 +46,38 @@ def check_format(file_path: Path) -> str: return "json" -def load_object_template(file_path: Path) -> Any: - template_format = check_format(file_path) +def load_object_template(file_path: Path, encoding: str) -> Any: + template_format = check_format(file_path, encoding) new_tileset = None new_tileset_path = None if template_format == "tmx": - with open(file_path) as template_file: + with open(file_path, encoding=encoding) as template_file: template = etree.parse(template_file).getroot() tileset_element = template.find("./tileset") if tileset_element is not None: tileset_path = Path(file_path.parent / tileset_element.attrib["source"]) - new_tileset = load_object_tileset(tileset_path) + new_tileset = load_object_tileset(tileset_path, encoding) new_tileset_path = tileset_path.parent else: - with open(file_path) as template_file: + with open(file_path, encoding=encoding) as template_file: template = json.load(template_file) if "tileset" in template: tileset_path = Path(file_path.parent / template["tileset"]["source"]) # type: ignore - new_tileset = load_object_tileset(tileset_path) + new_tileset = load_object_tileset(tileset_path, encoding) new_tileset_path = tileset_path.parent return (template, new_tileset, new_tileset_path) -def load_object_tileset(file_path: Path) -> Any: - tileset_format = check_format(file_path) +def load_object_tileset(file_path: Path, encoding: str) -> Any: + tileset_format = check_format(file_path, encoding) new_tileset = None - with open(file_path) as tileset_file: + with open(file_path, encoding=encoding) as tileset_file: if tileset_format == "tmx": new_tileset = etree.parse(tileset_file).getroot() else: diff --git a/pytiled_parser/world.py b/pytiled_parser/world.py index 67214a6a..64ddfed3 100644 --- a/pytiled_parser/world.py +++ b/pytiled_parser/world.py @@ -100,7 +100,7 @@ def _parse_world_map(raw_world_map: RawWorldMap, map_file: Path) -> WorldMap: ) -def parse_world(file: Path) -> World: +def parse_world(file: Path, encoding: str) -> World: """Parse the raw world into a pytiled_parser type Args: @@ -110,7 +110,7 @@ def parse_world(file: Path) -> World: World: A properly parsed [World][pytiled_parser.world.World] """ - with open(file) as world_file: + with open(file, encoding="utf-8") as world_file: raw_world = json.load(world_file) parent_dir = file.parent diff --git a/tests/test_data/layer_tests/group_layer_order/expected.py b/tests/test_data/layer_tests/group_layer_order/expected.py new file mode 100644 index 00000000..a3dcfa1a --- /dev/null +++ b/tests/test_data/layer_tests/group_layer_order/expected.py @@ -0,0 +1,59 @@ +from pathlib import Path + +from pytiled_parser import common_types, layer, tiled_object + +EXPECTED = [ + layer.ObjectLayer( + name="Outer 2", + opacity=1, + visible=True, + id=13, + draw_order="topdown", + tiled_objects=[], + parallax_factor=common_types.OrderedPair(1.0, 1.0), + ), + layer.LayerGroup( + name="Outer Group", + opacity=1, + visible=True, + id=4, + parallax_factor=common_types.OrderedPair(1.0, 1.0), + layers=[ + layer.ObjectLayer( + name="Inner 2", + opacity=1, + visible=True, + id=15, + draw_order="topdown", + tiled_objects=[], + parallax_factor=common_types.OrderedPair(1.0, 1.0), + ), + layer.LayerGroup( + name="Inner Group", + id=6, + layers=[], + visible=True, + opacity=1, + parallax_factor=common_types.OrderedPair(1.0, 1.0), + ), + layer.ObjectLayer( + name="Inner 1", + opacity=1, + visible=True, + id=14, + draw_order="topdown", + tiled_objects=[], + parallax_factor=common_types.OrderedPair(1.0, 1.0), + ), + ], + ), + layer.ObjectLayer( + name="Outer 1", + opacity=1, + visible=True, + id=12, + draw_order="topdown", + parallax_factor=common_types.OrderedPair(1.0, 1.0), + tiled_objects=[], + ), +] diff --git a/tests/test_data/layer_tests/group_layer_order/map.json b/tests/test_data/layer_tests/group_layer_order/map.json new file mode 100644 index 00000000..ad4f31ed --- /dev/null +++ b/tests/test_data/layer_tests/group_layer_order/map.json @@ -0,0 +1,84 @@ +{ "compressionlevel":-1, + "height":6, + "infinite":false, + "layers":[ + { + "draworder":"topdown", + "id":13, + "name":"Outer 2", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "id":4, + "layers":[ + { + "draworder":"topdown", + "id":15, + "name":"Inner 2", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "id":6, + "layers":[], + "name":"Inner Group", + "opacity":1, + "type":"group", + "visible":true, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":14, + "name":"Inner 1", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "name":"Outer Group", + "opacity":1, + "type":"group", + "visible":true, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":12, + "name":"Outer 1", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":16, + "nextobjectid":12, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.9.2", + "tileheight":32, + "tilesets":[ + { + "firstgid":1, + "source":"tileset.json" + }], + "tilewidth":32, + "type":"map", + "version":"1.9", + "width":8 +} \ No newline at end of file diff --git a/tests/test_data/layer_tests/group_layer_order/map.tmx b/tests/test_data/layer_tests/group_layer_order/map.tmx new file mode 100644 index 00000000..b76b2bc4 --- /dev/null +++ b/tests/test_data/layer_tests/group_layer_order/map.tmx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/tests/test_data/layer_tests/group_layer_order/tileset.json b/tests/test_data/layer_tests/group_layer_order/tileset.json new file mode 100644 index 00000000..0879fe64 --- /dev/null +++ b/tests/test_data/layer_tests/group_layer_order/tileset.json @@ -0,0 +1,14 @@ +{ "columns":8, + "image":"..\/..\/images\/tmw_desert_spacing.png", + "imageheight":199, + "imagewidth":265, + "margin":1, + "name":"tile_set_image", + "spacing":1, + "tilecount":48, + "tiledversion":"1.9.0", + "tileheight":32, + "tilewidth":32, + "type":"tileset", + "version":"1.8" +} \ No newline at end of file diff --git a/tests/test_data/layer_tests/group_layer_order/tileset.tsx b/tests/test_data/layer_tests/group_layer_order/tileset.tsx new file mode 100644 index 00000000..ebde4f19 --- /dev/null +++ b/tests/test_data/layer_tests/group_layer_order/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_data/map_tests/special_do_not_resave_from_tiled/expected.py b/tests/test_data/map_tests/special_do_not_resave_from_tiled/expected.py new file mode 100644 index 00000000..16356569 --- /dev/null +++ b/tests/test_data/map_tests/special_do_not_resave_from_tiled/expected.py @@ -0,0 +1,48 @@ +from pathlib import Path + +from pytiled_parser import common_types, tiled_map, tileset + +EXPECTED = tiled_map.TiledMap( + map_file=None, + infinite=False, + layers=[], + map_size=common_types.Size(8, 6), + next_layer_id=2, + next_object_id=1, + orientation="orthogonal", + render_order="right-down", + tiled_version="1.9.1", + tile_size=common_types.Size(32, 32), + version="1.9", + background_color=common_types.Color(255, 0, 4, 255), + parallax_origin=common_types.OrderedPair(10, 15), + tilesets={ + 1: tileset.Tileset( + columns=8, + image=Path(Path(__file__).parent / "../../images/tmw_desert_spacing.png") + .absolute() + .resolve(), + image_width=265, + image_height=199, + firstgid=1, + margin=1, + spacing=1, + name="tile_set_image", + tile_count=48, + tiled_version="1.6.0", + tile_height=32, + tile_width=32, + version="1.6", + type="tileset", + ) + }, + properties={ + "bool property - true": True, + "color property": common_types.Color(73, 252, 255, 255), + "file property": Path("../../../../../../var/log/syslog"), + "float property": 1.23456789, + "int property": 13, + "broken int property": 14, + "string property": "Hello, World!!", + }, +) diff --git a/tests/test_data/map_tests/special_do_not_resave_from_tiled/map.json b/tests/test_data/map_tests/special_do_not_resave_from_tiled/map.json new file mode 100644 index 00000000..6b147a85 --- /dev/null +++ b/tests/test_data/map_tests/special_do_not_resave_from_tiled/map.json @@ -0,0 +1,59 @@ +{ "backgroundcolor":"#ff0004", + "compressionlevel":0, + "height":6, + "infinite":false, + "layers":[], + "nextlayerid":2, + "nextobjectid":1, + "orientation":"orthogonal", + "parallaxoriginx":10, + "parallaxoriginy":15, + "properties":[ + { + "name":"bool property - true", + "type":"bool", + "value":true + }, + { + "name":"color property", + "type":"color", + "value":"#ff49fcff" + }, + { + "name":"file property", + "type":"file", + "value":"..\/..\/..\/..\/..\/..\/var\/log\/syslog" + }, + { + "name":"float property", + "type":"float", + "value":1.23456789 + }, + { + "name":"int property", + "type":"int", + "value":13 + }, + { + "name":"broken int property", + "type":"int", + "value":13.6 + }, + { + "name":"string property", + "type":"string", + "value":"Hello, World!!" + }], + "renderorder":"right-down", + "tiledversion":"1.9.1", + "tileheight":32, + "tilesets":[ + { + "firstgid":1, + "source":"tileset.json" + }], + "tilewidth":32, + "type":"map", + "version":"1.9", + "width":8 +} \ No newline at end of file diff --git a/tests/test_data/map_tests/special_do_not_resave_from_tiled/map.tmx b/tests/test_data/map_tests/special_do_not_resave_from_tiled/map.tmx new file mode 100644 index 00000000..ecdd0f87 --- /dev/null +++ b/tests/test_data/map_tests/special_do_not_resave_from_tiled/map.tmx @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/tests/test_data/map_tests/special_do_not_resave_from_tiled/tileset.json b/tests/test_data/map_tests/special_do_not_resave_from_tiled/tileset.json new file mode 100644 index 00000000..0879fe64 --- /dev/null +++ b/tests/test_data/map_tests/special_do_not_resave_from_tiled/tileset.json @@ -0,0 +1,14 @@ +{ "columns":8, + "image":"..\/..\/images\/tmw_desert_spacing.png", + "imageheight":199, + "imagewidth":265, + "margin":1, + "name":"tile_set_image", + "spacing":1, + "tilecount":48, + "tiledversion":"1.9.0", + "tileheight":32, + "tilewidth":32, + "type":"tileset", + "version":"1.8" +} \ No newline at end of file diff --git a/tests/test_data/map_tests/special_do_not_resave_from_tiled/tileset.tsx b/tests/test_data/map_tests/special_do_not_resave_from_tiled/tileset.tsx new file mode 100644 index 00000000..8b1cf24b --- /dev/null +++ b/tests/test_data/map_tests/special_do_not_resave_from_tiled/tileset.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_data/map_tests/template/expected.py b/tests/test_data/map_tests/template/expected.py index a2266e8e..b2641af5 100644 --- a/tests/test_data/map_tests/template/expected.py +++ b/tests/test_data/map_tests/template/expected.py @@ -15,13 +15,13 @@ tiled_objects=[ tiled_object.Rectangle( id=2, - name="", + name="hello", rotation=0, size=common_types.Size(63.6585878103079, 38.2811778048473), coordinates=common_types.OrderedPair( 98.4987608686521, 46.2385012811358 ), - properties={"test": "hello"}, + properties={"test": "hello", "testtest": "fromtemplate"}, visible=True, class_="", ), @@ -110,6 +110,32 @@ image_width=32, width=32, height=32, + objects=layer.ObjectLayer( + id=2, + name="", + draw_order="index", + opacity=1, + visible=True, + parallax_factor=common_types.OrderedPair(1.0, 1.0), + coordinates=common_types.OrderedPair(0, 0), + tiled_objects=[ + tiled_object.Rectangle( + id=1, + name="test", + rotation=0, + size=common_types.Size( + 63.6585878103079, 38.2811778048473 + ), + coordinates=common_types.OrderedPair(9.5, 3.0), + properties={ + "test": "world", + "testtest": "fromtemplate", + }, + visible=True, + class_="", + ), + ], + ), ) }, tile_count=1, diff --git a/tests/test_data/map_tests/template/map.json b/tests/test_data/map_tests/template/map.json index b372362d..25cab9e5 100644 --- a/tests/test_data/map_tests/template/map.json +++ b/tests/test_data/map_tests/template/map.json @@ -17,6 +17,7 @@ "value":"hello" }], "template":"template-rectangle.json", + "name": "hello", "x":98.4987608686521, "y":46.2385012811358 }, diff --git a/tests/test_data/map_tests/template/map.tmx b/tests/test_data/map_tests/template/map.tmx index b2138990..18c0a273 100644 --- a/tests/test_data/map_tests/template/map.tmx +++ b/tests/test_data/map_tests/template/map.tmx @@ -11,7 +11,7 @@ - + diff --git a/tests/test_data/map_tests/template/template-rectangle.json b/tests/test_data/map_tests/template/template-rectangle.json index 3bed3fa1..446855a0 100644 --- a/tests/test_data/map_tests/template/template-rectangle.json +++ b/tests/test_data/map_tests/template/template-rectangle.json @@ -2,11 +2,23 @@ { "height":38.2811778048473, "id":1, - "name":"", + "name":"test", "rotation":0, "class":"", "visible":true, - "width":63.6585878103079 + "width":63.6585878103079, + "properties":[ + { + "name":"test", + "type":"string", + "value":"world" + }, + { + "name":"testtest", + "type":"string", + "value":"fromtemplate" + } + ] }, "type":"template" } \ No newline at end of file diff --git a/tests/test_data/map_tests/template/template-rectangle.tx b/tests/test_data/map_tests/template/template-rectangle.tx index 6daa3644..94fe8e38 100644 --- a/tests/test_data/map_tests/template/template-rectangle.tx +++ b/tests/test_data/map_tests/template/template-rectangle.tx @@ -1,4 +1,9 @@ diff --git a/tests/test_data/map_tests/template/tile_set_single_image.json b/tests/test_data/map_tests/template/tile_set_single_image.json index 859e3da6..51341d00 100644 --- a/tests/test_data/map_tests/template/tile_set_single_image.json +++ b/tests/test_data/map_tests/template/tile_set_single_image.json @@ -9,16 +9,34 @@ "name":"tile_set_single_image", "spacing":0, "tilecount":1, - "tiledversion":"1.7.1", + "tiledversion":"1.9.2", "tileheight":32, "tiles":[ { "id":0, "image":"..\/..\/images\/tile_02.png", "imageheight":32, - "imagewidth":32 + "imagewidth":32, + "objectgroup": + { + "draworder":"index", + "id":2, + "name":"", + "objects":[ + { + "id":1, + "template":"template-rectangle.json", + "x":9.5, + "y":3 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + } }], "tilewidth":32, "type":"tileset", - "version":"1.6" + "version":"1.9" } \ No newline at end of file diff --git a/tests/test_data/map_tests/template/tile_set_single_image.tsx b/tests/test_data/map_tests/template/tile_set_single_image.tsx index c881c112..25cc8e74 100644 --- a/tests/test_data/map_tests/template/tile_set_single_image.tsx +++ b/tests/test_data/map_tests/template/tile_set_single_image.tsx @@ -1,7 +1,10 @@ - + + + + diff --git a/tests/test_layer.py b/tests/test_layer.py index 727fca45..7ce69d2e 100644 --- a/tests/test_layer.py +++ b/tests/test_layer.py @@ -1,4 +1,5 @@ """Tests for tilesets""" + import importlib.util import json import os @@ -24,6 +25,7 @@ LAYER_TESTS / "no_layers", LAYER_TESTS / "infinite_map", LAYER_TESTS / "infinite_map_b64", + LAYER_TESTS / "group_layer_order", ] ZSTD_LAYER_TEST = LAYER_TESTS / "b64_zstd" @@ -72,33 +74,27 @@ def test_layer_integration(parser_type, layer_test): raw_layers_path = layer_test / "map.json" with open(raw_layers_path) as raw_layers_file: raw_layers = json.load(raw_layers_file)["layers"] - layers = [parse_json(raw_layer) for raw_layer in raw_layers] + layers = [ + parse_json(raw_layer, encoding="utf-8") for raw_layer in raw_layers + ] elif parser_type == "tmx": raw_layers_path = layer_test / "map.tmx" with open(raw_layers_path) as raw_layers_file: - raw_layer = etree.parse(raw_layers_file).getroot() + raw_map = etree.parse(raw_layers_file).getroot() layers = [] - for layer in raw_layer.findall("./layer"): - layers.append(parse_tmx(layer)) - - for layer in raw_layer.findall("./objectgroup"): - layers.append(parse_tmx(layer)) - - for layer in raw_layer.findall("./group"): - layers.append(parse_tmx(layer)) - - for layer in raw_layer.findall("./imagelayer"): - layers.append(parse_tmx(layer)) + for element in raw_map: + if element.tag in ["layer", "objectgroup", "imagelayer", "group"]: + layers.append(parse_tmx(element, encoding="utf-8")) for layer in layers: fix_layer(layer) for layer in expected.EXPECTED: fix_layer(layer) - print(layer.size) assert layers == expected.EXPECTED + @pytest.mark.parametrize("parser_type", ["json", "tmx"]) def test_zstd_not_installed(parser_type): if parser_type == "json": @@ -106,7 +102,10 @@ def test_zstd_not_installed(parser_type): with open(raw_layers_path) as raw_layers_file: raw_layers = json.load(raw_layers_file)["layers"] with pytest.raises(ValueError): - layers = [parse_json(raw_layer) for raw_layer in raw_layers] + layers = [ + parse_json(raw_layer, encoding="utf-8") + for raw_layer in reversed(raw_layers) + ] elif parser_type == "tmx": raw_layers_path = ZSTD_LAYER_TEST / "map.tmx" with open(raw_layers_path) as raw_layers_file: @@ -114,25 +113,28 @@ def test_zstd_not_installed(parser_type): raw_layer = etree.parse(raw_layers_file).getroot() layers = [] for layer in raw_layer.findall("./layer"): - layers.append(parse_tmx(layer)) + layers.append(parse_tmx(layer, encoding="utf-8")) for layer in raw_layer.findall("./objectgroup"): - layers.append(parse_tmx(layer)) + layers.append(parse_tmx(layer, encoding="utf-8")) for layer in raw_layer.findall("./group"): - layers.append(parse_tmx(layer)) + layers.append(parse_tmx(layer, encoding="utf-8")) for layer in raw_layer.findall("./imagelayer"): - layers.append(parse_tmx(layer)) + layers.append(parse_tmx(layer, encoding="utf-8")) + def test_unknown_layer_type(): # We only test JSON here because due to the nature of the TMX format # there does not exist a scenario where pytiled_parser can attempt to - # parse an unknown layer type. In JSON a RuntimeError error will be + # parse an unknown layer type. In JSON a RuntimeError error will be # raised if an unknown type is provided. In TMX the layer will just # be ignored. raw_layers_path = UNKNOWN_LAYER_TYPE_TEST / "map.json" with open(raw_layers_path) as raw_layers_file: raw_layers = json.load(raw_layers_file)["layers"] with pytest.raises(RuntimeError): - layers = [parse_json(raw_layer) for raw_layer in raw_layers] + layers = [ + parse_json(raw_layer, encoding="utf-8") for raw_layer in raw_layers + ] diff --git a/tests/test_map.py b/tests/test_map.py index 6083d0e3..4fe5a3b4 100644 --- a/tests/test_map.py +++ b/tests/test_map.py @@ -1,4 +1,5 @@ """Tests for maps""" + import importlib.util import os from pathlib import Path @@ -20,10 +21,12 @@ MAP_TESTS / "embedded_tileset", MAP_TESTS / "template", MAP_TESTS / "cross_format_tileset", + MAP_TESTS / "special_do_not_resave_from_tiled", ] JSON_INVALID_TILESET = MAP_TESTS / "json_invalid_tileset" + def fix_object(my_object): my_object.coordinates = OrderedPair( round(my_object.coordinates[0], 3), round(my_object.coordinates[1], 3) @@ -90,6 +93,7 @@ def test_map_integration(parser_type, map_test): fix_map(casted_map) assert casted_map == expected.EXPECTED + def test_json_invalid_tileset(): raw_map_path = JSON_INVALID_TILESET / "map.json" diff --git a/tests/test_tiled_object_json.py b/tests/test_tiled_object_json.py index 90daf31b..16d24737 100644 --- a/tests/test_tiled_object_json.py +++ b/tests/test_tiled_object_json.py @@ -1,4 +1,5 @@ """Tests for objects""" + import json from contextlib import ExitStack as does_not_raise from pathlib import Path @@ -1110,7 +1111,7 @@ @pytest.mark.parametrize("raw_object_json,expected", OBJECTS) def test_parse_layer(raw_object_json, expected): raw_object = json.loads(raw_object_json) - result = parse(raw_object) + result = parse(raw_object, encoding="utf-8") assert result == expected @@ -1128,4 +1129,4 @@ def test_parse_no_parent_dir(): json_object = json.loads(raw_object) with pytest.raises(RuntimeError): - parse(json_object) + parse(json_object, encoding="utf-8") diff --git a/tests/test_tiled_object_tmx.py b/tests/test_tiled_object_tmx.py index 702ced32..0b6fd070 100644 --- a/tests/test_tiled_object_tmx.py +++ b/tests/test_tiled_object_tmx.py @@ -1,4 +1,5 @@ """Tests for objects""" + import xml.etree.ElementTree as etree from contextlib import ExitStack as does_not_raise from pathlib import Path @@ -129,6 +130,9 @@ + Hi +I can write multiple lines in here +That's pretty great """, @@ -144,6 +148,7 @@ "float property": 42.1, "int property": 8675309, "string property": "pytiled_parser rulez!1!!", + "multiline string property": "Hi\nI can write multiple lines in here\nThat's pretty great", }, ), ), @@ -487,6 +492,6 @@ @pytest.mark.parametrize("raw_object_tmx,expected", OBJECTS) def test_parse_layer(raw_object_tmx, expected): raw_object = etree.fromstring(raw_object_tmx) - result = parse(raw_object) + result = parse(raw_object, encoding="utf-8") assert result == expected diff --git a/tests/test_world.py b/tests/test_world.py index 1bed9817..8224e035 100644 --- a/tests/test_world.py +++ b/tests/test_world.py @@ -1,4 +1,5 @@ """Tests for worlds""" + import importlib.util import operator import os @@ -35,7 +36,7 @@ def test_world_integration(world_test): raw_world_path = world_test / "world.world" - casted_world = world.parse_world(raw_world_path) + casted_world = world.parse_world(raw_world_path, encoding="utf-8") # These fix calls sort the map list in the world by the map_file # attribute because we don't actually care about the order of the list