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 0c6b8b24..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.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 299c0ad3..a6c4f148 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,28 @@ 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.
diff --git a/README.md b/README.md
index e06358b0..faba7630 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ integrate PyTiled Parser and [example code](https://api.arcade.academy/en/latest
## Supported Python Versions
-pytiled-parser works on Python 3.6 - 3.12(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.
+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
diff --git a/pyproject.toml b/pyproject.toml
index f520bb6e..249e89fc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,45 +1,39 @@
[project]
name = "pytiled_parser"
-version = "2.2.6"
+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/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 527cf15e..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,7 +315,9 @@ 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"]
@@ -334,6 +337,9 @@ def parse(
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:
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 a7af55a6..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.findall("./*"):
+ 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)
diff --git a/pytiled_parser/parsers/tmx/tiled_object.py b/pytiled_parser/parsers/tmx/tiled_object.py
index a2e16d53..a36cfd1a 100644
--- a/pytiled_parser/parsers/tmx/tiled_object.py
+++ b/pytiled_parser/parsers/tmx/tiled_object.py
@@ -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")
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/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 7470be29..b2641af5 100644
--- a/tests/test_data/map_tests/template/expected.py
+++ b/tests/test_data/map_tests/template/expected.py
@@ -15,7 +15,7 @@
tiled_objects=[
tiled_object.Rectangle(
id=2,
- name="",
+ name="hello",
rotation=0,
size=common_types.Size(63.6585878103079, 38.2811778048473),
coordinates=common_types.OrderedPair(
@@ -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 @@
-