diff --git a/addons/vnen.tiled_importer/plugin.cfg b/addons/vnen.tiled_importer/plugin.cfg new file mode 100644 index 0000000..e0b9ec7 --- /dev/null +++ b/addons/vnen.tiled_importer/plugin.cfg @@ -0,0 +1,8 @@ +config_version=3 +[plugin] + +name="Tiled Map Importer" +description="Importer for TileMaps and TileSets made on Tiled Map Editor" +version="2.4" +author="George Marques" +script="vnen.tiled_importer.gd" diff --git a/addons/vnen.tiled_importer/polygon_sorter.gd b/addons/vnen.tiled_importer/polygon_sorter.gd new file mode 100644 index 0000000..d646d0b --- /dev/null +++ b/addons/vnen.tiled_importer/polygon_sorter.gd @@ -0,0 +1,67 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 George Marques +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# Sorter for polygon vertices +tool +extends Reference + +var center + +# Sort the vertices of a convex polygon to clockwise order +# Receives a PoolVector2Array and returns a new one +func sort_polygon(vertices): + vertices = Array(vertices) + + var centroid = Vector2() + var size = vertices.size() + + for i in range(0, size): + centroid += vertices[i] + + centroid /= size + + center = centroid + vertices.sort_custom(self, "is_less") + + return PoolVector2Array(vertices) + +# Sorter function, determines which of the poins should come first +func is_less(a, b): + if a.x - center.x >= 0 and b.x - center.x < 0: + return false + elif a.x - center.x < 0 and b.x - center.x >= 0: + return true + elif a.x - center.x == 0 and b.x - center.x == 0: + if a.y - center.y >= 0 or b.y - center.y >= 0: + return a.y < b.y + return a.y > b.y + + var det = (a.x - center.x) * (b.y - center.y) - (b.x - center.x) * (a.y - center.y) + if det > 0: + return true + elif det < 0: + return false + + var d1 = (a - center).length_squared() + var d2 = (b - center).length_squared() + + return d1 < d2 diff --git a/addons/vnen.tiled_importer/tiled.png b/addons/vnen.tiled_importer/tiled.png new file mode 100644 index 0000000..17d7d49 Binary files /dev/null and b/addons/vnen.tiled_importer/tiled.png differ diff --git a/addons/vnen.tiled_importer/tiled.png.import b/addons/vnen.tiled_importer/tiled.png.import new file mode 100644 index 0000000..6d82870 --- /dev/null +++ b/addons/vnen.tiled_importer/tiled.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/tiled.png-dbbabe58e4f927769540a522a2073689.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/vnen.tiled_importer/tiled.png" +dest_files=[ "res://.import/tiled.png-dbbabe58e4f927769540a522a2073689.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=false +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=false +svg/scale=1.0 diff --git a/addons/vnen.tiled_importer/tiled_import_plugin.gd b/addons/vnen.tiled_importer/tiled_import_plugin.gd new file mode 100644 index 0000000..774b65d --- /dev/null +++ b/addons/vnen.tiled_importer/tiled_import_plugin.gd @@ -0,0 +1,148 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 George Marques +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +tool +extends EditorImportPlugin + +enum { PRESET_DEFAULT, PRESET_PIXEL_ART } + +const TiledMapReader = preload("tiled_map_reader.gd") + +func get_importer_name(): + return "vnen.tiled_importer" + +func get_visible_name(): + return "Scene from Tiled" + +func get_recognized_extensions(): + if ProjectSettings.get_setting("tiled_importer/enable_json_format"): + return ["json", "tmx"] + else: + return ["tmx"] + +func get_save_extension(): + return "scn" + +func get_priority(): + return 1 + +func get_import_order(): + return 101 + +func get_resource_type(): + return "PackedScene" + +func get_preset_count(): + return 2 + +func get_preset_name(preset): + match preset: + PRESET_DEFAULT: return "Default" + PRESET_PIXEL_ART: return "Pixel Art" + +func get_import_options(preset): + return [ + { + "name": "custom_properties", + "default_value": true + }, + { + "name": "tile_metadata", + "default_value": false + }, + { + "name": "uv_clip", + "default_value": true + }, + { + "name": "y_sort", + "default_value": true + }, + { + "name": "image_flags", + "default_value": 0 if preset == PRESET_PIXEL_ART else Texture.FLAGS_DEFAULT, + "property_hint": PROPERTY_HINT_FLAGS, + "hint_string": "Mipmaps,Repeat,Filter,Anisotropic,sRGB,Mirrored Repeat" + }, + { + "name": "collision_layer", + "default_value": 1, + "property_hint": PROPERTY_HINT_LAYERS_2D_PHYSICS + }, + { + "name": "collision_mask", + "default_value": 1, + "property_hint": PROPERTY_HINT_LAYERS_2D_PHYSICS + }, + { + "name": "embed_internal_images", + "default_value": true if preset == PRESET_PIXEL_ART else false + }, + { + "name": "save_tiled_properties", + "default_value": false + }, + { + "name": "add_background", + "default_value": true + }, + { + "name": "post_import_script", + "default_value": "", + "property_hint": PROPERTY_HINT_FILE, + "hint_string": "*.gd;GDScript" + } + ] + +func get_option_visibility(option, options): + return true + +func import(source_file, save_path, options, r_platform_variants, r_gen_files): + var map_reader = TiledMapReader.new() + + var scene = map_reader.build(source_file, options) + + if typeof(scene) != TYPE_OBJECT: + # Error happened + return scene + + # Post imports script + if not options.post_import_script.empty(): + var script = load(options.post_import_script) + if not script or not script is GDScript: + printerr("Post import script is not a GDScript.") + return ERR_INVALID_PARAMETER + + script = script.new() + if not script.has_method("post_import"): + printerr("Post import script does not have a 'post_import' method.") + return ERR_INVALID_PARAMETER + + scene = script.post_import(scene) + + if not scene or not scene is Node2D: + printerr("Invalid scene returned from post import script.") + return ERR_INVALID_DATA + + var packed_scene = PackedScene.new() + packed_scene.pack(scene) + return ResourceSaver.save("%s.%s" % [save_path, get_save_extension()], packed_scene) diff --git a/addons/vnen.tiled_importer/tiled_map_reader.gd b/addons/vnen.tiled_importer/tiled_map_reader.gd new file mode 100644 index 0000000..7f0a019 --- /dev/null +++ b/addons/vnen.tiled_importer/tiled_map_reader.gd @@ -0,0 +1,1412 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 George Marques +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +tool +extends Reference + +# Constants for tile flipping +# http://doc.mapeditor.org/reference/tmx-map-format/#tile-flipping +const FLIPPED_HORIZONTALLY_FLAG = 0x80000000 +const FLIPPED_VERTICALLY_FLAG = 0x40000000 +const FLIPPED_DIAGONALLY_FLAG = 0x20000000 + +# XML Format reader +const TiledXMLToDictionary = preload("tiled_xml_to_dict.gd") + +# Polygon vertices sorter +const PolygonSorter = preload("polygon_sorter.gd") + +# Prefix for error messages, make easier to identify the source +const error_prefix = "Tiled Importer: " + +# Properties to save the value in the metadata +const whitelist_properties = [ + "backgroundcolor", + "compression", + "draworder", + "gid", + "height", + "imageheight", + "imagewidth", + "infinite", + "margin", + "name", + "offsetx", + "offsety", + "orientation", + "probability", + "spacing", + "tilecount", + "tiledversion", + "tileheight", + "tilewidth", + "type", + "version", + "visible", + "width", + "custom_material" +] + +# All templates loaded, can be looked up by path name +var _loaded_templates = {} +# Maps each tileset file used by the map to it's first gid; Used for template parsing +var _tileset_path_to_first_gid = {} + +func reset_global_memebers(): + _loaded_templates = {} + _tileset_path_to_first_gid = {} + +# Main function +# Reads a source file and gives back a scene +func build(source_path, options): + reset_global_memebers() + var map = read_file(source_path) + if typeof(map) == TYPE_INT: + return map + if typeof(map) != TYPE_DICTIONARY: + return ERR_INVALID_DATA + + var err = validate_map(map) + if err != OK: + return err + + var cell_size = Vector2(int(map.tilewidth), int(map.tileheight)) + var map_mode = TileMap.MODE_SQUARE + var map_offset = TileMap.HALF_OFFSET_DISABLED + var map_pos_offset = Vector2() + var map_background = Color() + var cell_offset = Vector2() + if "orientation" in map: + match map.orientation: + "isometric": + map_mode = TileMap.MODE_ISOMETRIC + "staggered": + map_pos_offset.y -= cell_size.y / 2 + match map.staggeraxis: + "x": + map_offset = TileMap.HALF_OFFSET_Y + cell_size.x /= 2.0 + if map.staggerindex == "even": + cell_offset.x += 1 + map_pos_offset.x -= cell_size.x + "y": + map_offset = TileMap.HALF_OFFSET_X + cell_size.y /= 2.0 + if map.staggerindex == "even": + cell_offset.y += 1 + map_pos_offset.y -= cell_size.y + "hexagonal": + # Godot maps are always odd and don't have an "even" setting. To + # imitate even staggering we simply start one row/column late and + # adjust the position of the whole map. + match map.staggeraxis: + "x": + map_offset = TileMap.HALF_OFFSET_Y + cell_size.x = int((cell_size.x + map.hexsidelength) / 2) + if map.staggerindex == "even": + cell_offset.x += 1 + map_pos_offset.x -= cell_size.x + "y": + map_offset = TileMap.HALF_OFFSET_X + cell_size.y = int((cell_size.y + map.hexsidelength) / 2) + if map.staggerindex == "even": + cell_offset.y += 1 + map_pos_offset.y -= cell_size.y + + var tileset = build_tileset_for_scene(map.tilesets, source_path, options) + if typeof(tileset) != TYPE_OBJECT: + # Error happened + return tileset + + var root = Node2D.new() + root.set_name(source_path.get_file().get_basename()) + if options.save_tiled_properties: + set_tiled_properties_as_meta(root, map) + if options.custom_properties: + set_custom_properties(root, map) + + var map_data = { + "options": options, + "map_mode": map_mode, + "map_offset": map_offset, + "map_pos_offset": map_pos_offset, + "map_background": map_background, + "cell_size": cell_size, + "cell_offset": cell_offset, + "tileset": tileset, + "source_path": source_path, + "infinite": bool(map.infinite) if "infinite" in map else false + } + + for layer in map.layers: + err = make_layer(layer, root, root, map_data) + if err != OK: + return err + + if options.add_background and "backgroundcolor" in map: + var bg_color = str(map.backgroundcolor) + if (!bg_color.is_valid_html_color()): + print_error("Invalid background color format: " + bg_color) + return root + + map_background = Color(bg_color) + + var viewport_size = Vector2(ProjectSettings.get("display/window/size/width"), ProjectSettings.get("display/window/size/height")) + var parbg = ParallaxBackground.new() + var parlayer = ParallaxLayer.new() + var colorizer = ColorRect.new() + + parbg.scroll_ignore_camera_zoom = true + parlayer.motion_mirroring = viewport_size + colorizer.color = map_background + colorizer.rect_size = viewport_size + colorizer.rect_min_size = viewport_size + + parbg.name = "Background" + root.add_child(parbg) + parbg.owner = root + parlayer.name = "BackgroundLayer" + parbg.add_child(parlayer) + parlayer.owner = root + colorizer.name = "BackgroundColor" + parlayer.add_child(colorizer) + colorizer.owner = root + + return root + +# Creates a layer node from the data +# Returns an error code +func make_layer(layer, parent, root, data): + var err = validate_layer(layer) + if err != OK: + return err + + # Main map data + var map_mode = data.map_mode + var map_offset = data.map_offset + var map_pos_offset = data.map_pos_offset + var cell_size = data.cell_size + var cell_offset = data.cell_offset + var options = data.options + var tileset = data.tileset + var source_path = data.source_path + var infinite = data.infinite + + var opacity = float(layer.opacity) if "opacity" in layer else 1.0 + var visible = bool(layer.visible) if "visible" in layer else true + + var z_index = 0 + + if "properties" in layer and "z_index" in layer.properties: + z_index = layer.properties.z_index + + if layer.type == "tilelayer": + var layer_size = Vector2(int(layer.width), int(layer.height)) + var tilemap = TileMap.new() + tilemap.set_name(str(layer.name)) + tilemap.cell_size = cell_size + tilemap.modulate = Color(1.0, 1.0, 1.0, opacity); + tilemap.visible = visible + tilemap.mode = map_mode + tilemap.cell_half_offset = map_offset + tilemap.format = 1 + tilemap.cell_clip_uv = options.uv_clip + tilemap.cell_y_sort = options.y_sort + tilemap.collision_layer = options.collision_layer + tilemap.collision_mask = options.collision_mask + tilemap.z_index = z_index + + var offset = Vector2() + if "offsetx" in layer: + offset.x = int(layer.offsetx) + if "offsety" in layer: + offset.y = int(layer.offsety) + + tilemap.position = offset + map_pos_offset + tilemap.tile_set = tileset + + var chunks = [] + + if infinite: + chunks = layer.chunks + else: + chunks = [layer] + + for chunk in chunks: + err = validate_chunk(chunk) + if err != OK: + return err + + var chunk_data = chunk.data + + if "encoding" in layer and layer.encoding == "base64": + if "compression" in layer: + chunk_data = decompress_layer_data(chunk.data, layer.compression, layer_size) + if typeof(chunk_data) == TYPE_INT: + # Error happened + return chunk_data + else: + chunk_data = read_base64_layer_data(chunk.data) + + var count = 0 + for tile_id in chunk_data: + var int_id = int(str(tile_id)) & 0xFFFFFFFF + + if int_id == 0: + count += 1 + continue + + var flipped_h = bool(int_id & FLIPPED_HORIZONTALLY_FLAG) + var flipped_v = bool(int_id & FLIPPED_VERTICALLY_FLAG) + var flipped_d = bool(int_id & FLIPPED_DIAGONALLY_FLAG) + + var gid = int_id & ~(FLIPPED_HORIZONTALLY_FLAG | FLIPPED_VERTICALLY_FLAG | FLIPPED_DIAGONALLY_FLAG) + + var cell_x = cell_offset.x + chunk.x + (count % int(chunk.width)) + var cell_y = cell_offset.y + chunk.y + int(count / chunk.width) + tilemap.set_cell(cell_x, cell_y, gid, flipped_h, flipped_v, flipped_d) + + count += 1 + + if options.save_tiled_properties: + set_tiled_properties_as_meta(tilemap, layer) + if options.custom_properties: + set_custom_properties(tilemap, layer) + + tilemap.set("editor/display_folded", true) + parent.add_child(tilemap) + tilemap.set_owner(root) + elif layer.type == "imagelayer": + var image = null + if layer.image != "": + image = load_image(layer.image, source_path, options) + if typeof(image) != TYPE_OBJECT: + # Error happened + return image + + var pos = Vector2() + var offset = Vector2() + + if "x" in layer: + pos.x = float(layer.x) + if "y" in layer: + pos.y = float(layer.y) + if "offsetx" in layer: + offset.x = float(layer.offsetx) + if "offsety" in layer: + offset.y = float(layer.offsety) + + var sprite = Sprite.new() + sprite.set_name(str(layer.name)) + sprite.centered = false + sprite.texture = image + sprite.visible = visible + sprite.modulate = Color(1.0, 1.0, 1.0, opacity) + sprite.z_index = z_index + if options.save_tiled_properties: + set_tiled_properties_as_meta(sprite, layer) + if options.custom_properties: + set_custom_properties(sprite, layer) + + sprite.set("editor/display_folded", true) + parent.add_child(sprite) + sprite.position = pos + offset + sprite.set_owner(root) + elif layer.type == "objectgroup": + var object_layer = Node2D.new() + if options.save_tiled_properties: + set_tiled_properties_as_meta(object_layer, layer) + if options.custom_properties: + set_custom_properties(object_layer, layer) + object_layer.modulate = Color(1.0, 1.0, 1.0, opacity) + object_layer.visible = visible + object_layer.z_index = z_index + object_layer.set("editor/display_folded", true) + parent.add_child(object_layer) + object_layer.set_owner(root) + if "name" in layer and not str(layer.name).empty(): + object_layer.set_name(str(layer.name)) + + if not "draworder" in layer or layer.draworder == "topdown": + layer.objects.sort_custom(self, "object_sorter") + + for object in layer.objects: + if "template" in object: + var template_file = object["template"] + var template_data_immutable = get_template(remove_filename_from_path(data["source_path"]) + template_file) + if typeof(template_data_immutable) != TYPE_DICTIONARY: + # Error happened + print("Error getting template for object with id " + str(data["id"])) + continue + + # Overwrite template data with current object data + apply_template(object, template_data_immutable) + + set_default_obj_params(object) + + if "point" in object and object.point: + var point = Position2D.new() + if not "x" in object or not "y" in object: + print_error("Missing coordinates for point in object layer.") + continue + point.position = Vector2(float(object.x), float(object.y)) + point.visible = bool(object.visible) if "visible" in object else true + object_layer.add_child(point) + point.set_owner(root) + if "name" in object and not str(object.name).empty(): + point.set_name(str(object.name)) + elif "id" in object and not str(object.id).empty(): + point.set_name(str(object.id)) + if options.save_tiled_properties: + set_tiled_properties_as_meta(point, object) + if options.custom_properties: + set_custom_properties(point, object) + + elif not "gid" in object: + # Not a tile object + if "type" in object and object.type == "navigation": + # Can't make navigation objects right now + print_error("Navigation polygons aren't supported in an object layer.") + continue # Non-fatal error + var shape = shape_from_object(object) + + if typeof(shape) != TYPE_OBJECT: + # Error happened + return shape + + if "type" in object and object.type == "occluder": + var occluder = LightOccluder2D.new() + var pos = Vector2() + var rot = 0 + + if "x" in object: + pos.x = float(object.x) + if "y" in object: + pos.y = float(object.y) + if "rotation" in object: + rot = float(object.rotation) + + occluder.visible = bool(object.visible) if "visible" in object else true + occluder.position = pos + occluder.rotation_degrees = rot + occluder.occluder = shape + if "name" in object and not str(object.name).empty(): + occluder.set_name(str(object.name)) + elif "id" in object and not str(object.id).empty(): + occluder.set_name(str(object.id)) + + if options.save_tiled_properties: + set_tiled_properties_as_meta(occluder, object) + if options.custom_properties: + set_custom_properties(occluder, object) + + object_layer.add_child(occluder) + occluder.set_owner(root) + + else: + var body = Area2D.new() if object.type == "area" else StaticBody2D.new() + + var offset = Vector2() + var collision + var pos = Vector2() + var rot = 0 + + if not ("polygon" in object or "polyline" in object): + # Regular shape + collision = CollisionShape2D.new() + collision.shape = shape + if shape is RectangleShape2D: + offset = shape.extents + elif shape is CircleShape2D: + offset = Vector2(shape.radius, shape.radius) + elif shape is CapsuleShape2D: + offset = Vector2(shape.radius, shape.height) + if shape.radius > shape.height: + var temp = shape.radius + shape.radius = shape.height + shape.height = temp + collision.rotation_degrees = 90 + shape.height *= 2 + collision.position = offset + else: + collision = CollisionPolygon2D.new() + var points = null + if shape is ConcavePolygonShape2D: + points = [] + var segments = shape.segments + for i in range(0, segments.size()): + if i % 2 != 0: + continue + points.push_back(segments[i]) + collision.build_mode = CollisionPolygon2D.BUILD_SEGMENTS + else: + points = shape.points + collision.build_mode = CollisionPolygon2D.BUILD_SOLIDS + collision.polygon = points + + collision.one_way_collision = object.type == "one-way" + + if "x" in object: + pos.x = float(object.x) + if "y" in object: + pos.y = float(object.y) + if "rotation" in object: + rot = float(object.rotation) + + body.set("editor/display_folded", true) + object_layer.add_child(body) + body.set_owner(root) + body.add_child(collision) + collision.set_owner(root) + + if options.save_tiled_properties: + set_tiled_properties_as_meta(body, object) + if options.custom_properties: + set_custom_properties(body, object) + + if "name" in object and not str(object.name).empty(): + body.set_name(str(object.name)) + elif "id" in object and not str(object.id).empty(): + body.set_name(str(object.id)) + body.visible = bool(object.visible) if "visible" in object else true + body.position = pos + body.rotation_degrees = rot + + else: # "gid" in object + var tile_raw_id = int(str(object.gid)) & 0xFFFFFFFF + var tile_id = tile_raw_id & ~(FLIPPED_HORIZONTALLY_FLAG | FLIPPED_VERTICALLY_FLAG | FLIPPED_DIAGONALLY_FLAG) + + var is_tile_object = tileset.tile_get_region(tile_id).get_area() == 0 + var collisions = tileset.tile_get_shape_count(tile_id) + var has_collisions = collisions > 0 && object.has("type") && object.type != "sprite" + var sprite = Sprite.new() + var pos = Vector2() + var rot = 0 + var scale = Vector2(1, 1) + sprite.texture = tileset.tile_get_texture(tile_id) + var texture_size = sprite.texture.get_size() if sprite.texture != null else Vector2() + + if not is_tile_object: + sprite.region_enabled = true + sprite.region_rect = tileset.tile_get_region(tile_id) + texture_size = tileset.tile_get_region(tile_id).size + + sprite.flip_h = bool(tile_raw_id & FLIPPED_HORIZONTALLY_FLAG) + sprite.flip_v = bool(tile_raw_id & FLIPPED_VERTICALLY_FLAG) + + if "x" in object: + pos.x = float(object.x) + if "y" in object: + pos.y = float(object.y) + if "rotation" in object: + rot = float(object.rotation) + if texture_size != Vector2(): + if "width" in object and float(object.width) != texture_size.x: + scale.x = float(object.width) / texture_size.x + if "height" in object and float(object.height) != texture_size.y: + scale.y = float(object.height) / texture_size.y + + var obj_root = sprite + if has_collisions: + match object.type: + "area": obj_root = Area2D.new() + "kinematic": obj_root = KinematicBody2D.new() + "rigid": obj_root = RigidBody2D.new() + _: obj_root = StaticBody2D.new() + + object_layer.add_child(obj_root) + obj_root.owner = root + + obj_root.add_child(sprite) + sprite.owner = root + + var shapes = tileset.tile_get_shapes(tile_id) + for s in shapes: + var collision_node = CollisionShape2D.new() + collision_node.shape = s.shape + + collision_node.transform = s.shape_transform + if sprite.flip_h: + collision_node.position.x *= -1 + collision_node.position.x -= cell_size.x + collision_node.scale.x *= -1 + if sprite.flip_v: + collision_node.scale.y *= -1 + collision_node.position.y *= -1 + collision_node.position.y -= cell_size.y + obj_root.add_child(collision_node) + collision_node.owner = root + + if "name" in object and not str(object.name).empty(): + obj_root.set_name(str(object.name)) + elif "id" in object and not str(object.id).empty(): + obj_root.set_name(str(object.id)) + + obj_root.position = pos + obj_root.rotation_degrees = rot + obj_root.visible = bool(object.visible) if "visible" in object else true + obj_root.scale = scale + # Translate from Tiled bottom-left position to Godot top-left + sprite.centered = false + sprite.region_filter_clip = options.uv_clip + sprite.offset = Vector2(0, -texture_size.y) + + if not has_collisions: + object_layer.add_child(sprite) + sprite.set_owner(root) + + if options.save_tiled_properties: + set_tiled_properties_as_meta(obj_root, object) + if options.custom_properties: + if options.tile_metadata: + var tile_meta = tileset.get_meta("tile_meta") + if typeof(tile_meta) == TYPE_DICTIONARY and tile_id in tile_meta: + for prop in tile_meta[tile_id]: + obj_root.set_meta(prop, tile_meta[tile_id][prop]) + set_custom_properties(obj_root, object) + + elif layer.type == "group": + var group = Node2D.new() + var pos = Vector2() + if "x" in layer: + pos.x = float(layer.x) + if "y" in layer: + pos.y = float(layer.y) + group.modulate = Color(1.0, 1.0, 1.0, opacity) + group.visible = visible + group.position = pos + group.z_index = z_index + + if options.save_tiled_properties: + set_tiled_properties_as_meta(group, layer) + if options.custom_properties: + set_custom_properties(group, layer) + + if "name" in layer and not str(layer.name).empty(): + group.set_name(str(layer.name)) + + group.set("editor/display_folded", true) + parent.add_child(group) + group.set_owner(root) + + for sub_layer in layer.layers: + make_layer(sub_layer, group, root, data) + + else: + print_error("Unknown layer type ('%s') in '%s'" % [str(layer.type), str(layer.name) if "name" in layer else "[unnamed layer]"]) + return ERR_INVALID_DATA + + return OK + +func set_default_obj_params(object): + # Set default values for object + for attr in ["width", "height", "rotation", "x", "y"]: + if not attr in object: + object[attr] = 0 + if not "type" in object: + object.type = "" + if not "visible" in object: + object.visible = true + +var flags + +# Makes a tileset from a array of tilesets data +# Since Godot supports only one TileSet per TileMap, all tilesets from Tiled are combined +func build_tileset_for_scene(tilesets, source_path, options): + var result = TileSet.new() + var err = ERR_INVALID_DATA + var tile_meta = {} + + for tileset in tilesets: + var ts = tileset + var ts_source_path = source_path + if "source" in ts: + if not "firstgid" in tileset or not str(tileset.firstgid).is_valid_integer(): + print_error("Missing or invalid firstgid tileset property.") + return ERR_INVALID_DATA + + ts_source_path = source_path.get_base_dir().plus_file(ts.source) + # Used later for templates + _tileset_path_to_first_gid[ts_source_path] = tileset.firstgid + + if ts.source.get_extension().to_lower() == "tsx": + var tsx_reader = TiledXMLToDictionary.new() + ts = tsx_reader.read_tsx(ts_source_path) + if typeof(ts) != TYPE_DICTIONARY: + # Error happened + return ts + else: # JSON Tileset + var f = File.new() + err = f.open(ts_source_path, File.READ) + if err != OK: + print_error("Error opening tileset '%s'." % [ts.source]) + return err + + var json_res = JSON.parse(f.get_as_text()) + if json_res.error != OK: + print_error("Error parsing tileset '%s' JSON: %s" % [ts.source, json_res.error_string]) + return ERR_INVALID_DATA + + ts = json_res.result + if typeof(ts) != TYPE_DICTIONARY: + print_error("Tileset '%s' is not a dictionary." % [ts.source]) + return ERR_INVALID_DATA + + ts.firstgid = tileset.firstgid + + err = validate_tileset(ts) + if err != OK: + return err + + var has_global_image = "image" in ts + + var spacing = int(ts.spacing) if "spacing" in ts and str(ts.spacing).is_valid_integer() else 0 + var margin = int(ts.margin) if "margin" in ts and str(ts.margin).is_valid_integer() else 0 + var firstgid = int(ts.firstgid) + var columns = int(ts.columns) if "columns" in ts and str(ts.columns).is_valid_integer() else -1 + + var image = null + var imagesize = Vector2() + + if has_global_image: + image = load_image(ts.image, ts_source_path, options) + if typeof(image) != TYPE_OBJECT: + # Error happened + return image + imagesize = Vector2(int(ts.imagewidth), int(ts.imageheight)) + + var tilesize = Vector2(int(ts.tilewidth), int(ts.tileheight)) + + var tilecount + if not "tilecount" in ts: + tilecount = make_tilecount(tilesize, imagesize, margin, spacing) + else: + tilecount = int(ts.tilecount) + + + var gid = firstgid + + var x = margin + var y = margin + + var i = 0 + var column = 0 + + + # Needed to look up textures for animations + var tileRegions = [] + while i < tilecount: + var tilepos = Vector2(x, y) + var region = Rect2(tilepos, tilesize) + + tileRegions.push_back(region) + + column += 1 + i += 1 + + x += int(tilesize.x) + spacing + if (columns > 0 and column >= columns) or x >= int(imagesize.x) - margin or (x + int(tilesize.x)) > int(imagesize.x): + x = margin + y += int(tilesize.y) + spacing + column = 0 + + i = 0 + + while i < tilecount: + var region = tileRegions[i] + + var rel_id = str(gid - firstgid) + + result.create_tile(gid) + + if has_global_image: + if rel_id in ts.tiles && "animation" in ts.tiles[rel_id]: + var animated_tex = AnimatedTexture.new() + animated_tex.frames = ts.tiles[rel_id].animation.size() + animated_tex.fps = 0 + var c = 0 + # Animated texture wants us to have seperate textures for each frame + # so we have to pull them out of the tileset + var tilesetTexture = image.get_data() + for g in ts.tiles[rel_id].animation: + var frameTex = tilesetTexture.get_rect(tileRegions[(int(g.tileid))]) + var newTex = ImageTexture.new() + newTex.create_from_image(frameTex, flags) + animated_tex.set_frame_texture(c, newTex) + animated_tex.set_frame_delay(c, float(g.duration) * 0.001) + c += 1 + result.tile_set_texture(gid, animated_tex) + result.tile_set_region(gid, Rect2(Vector2(0, 0), tilesize)) + else: + result.tile_set_texture(gid, image) + result.tile_set_region(gid, region) + elif not rel_id in ts.tiles: + gid += 1 + continue + else: + if rel_id in ts.tiles && "animation" in ts.tiles[rel_id]: + var animated_tex = AnimatedTexture.new() + animated_tex.frames = ts.tiles[rel_id].animation.size() + animated_tex.fps = 0 + var c = 0 + #untested + var image_path = ts.tiles[rel_id].image + for g in ts.tiles[rel_id].animation: + animated_tex.set_frame_texture(c, load_image(image_path, ts_source_path, options)) + animated_tex.set_frame_delay(c, float(g.duration) * 0.001) + c += 1 + result.tile_set_texture(gid, animated_tex) + result.tile_set_region(gid, Rect2(Vector2(0, 0), tilesize)) + else: + var image_path = ts.tiles[rel_id].image + image = load_image(image_path, ts_source_path, options) + if typeof(image) != TYPE_OBJECT: + # Error happened + return image + result.tile_set_texture(gid, image) + + if "tiles" in ts and rel_id in ts.tiles and "objectgroup" in ts.tiles[rel_id] \ + and "objects" in ts.tiles[rel_id].objectgroup: + for object in ts.tiles[rel_id].objectgroup.objects: + + var shape = shape_from_object(object) + + if typeof(shape) != TYPE_OBJECT: + # Error happened + return shape + + var offset = Vector2(float(object.x), float(object.y)) + if "width" in object and "height" in object: + offset += Vector2(float(object.width) / 2, float(object.height) / 2) + + if object.type == "navigation": + result.tile_set_navigation_polygon(gid, shape) + result.tile_set_navigation_polygon_offset(gid, offset) + elif object.type == "occluder": + result.tile_set_light_occluder(gid, shape) + result.tile_set_occluder_offset(gid, offset) + else: + result.tile_add_shape(gid, shape, Transform2D(0, offset), object.type == "one-way") + + if "properties" in ts and "custom_material" in ts.properties: + result.tile_set_material(gid, load(ts.properties.custom_material)) + + if options.custom_properties and options.tile_metadata and "tileproperties" in ts \ + and "tilepropertytypes" in ts and rel_id in ts.tileproperties and rel_id in ts.tilepropertytypes: + tile_meta[gid] = get_custom_properties(ts.tileproperties[rel_id], ts.tilepropertytypes[rel_id]) + if options.save_tiled_properties and rel_id in ts.tiles: + for property in whitelist_properties: + if property in ts.tiles[rel_id]: + if not gid in tile_meta: tile_meta[gid] = {} + tile_meta[gid][property] = ts.tiles[rel_id][property] + + # If tile has a custom property called 'name', set the tile's name + if property == "name": + result.tile_set_name(gid, ts.tiles[rel_id].properties.name) + + + gid += 1 + i += 1 + + if str(ts.name) != "": + result.resource_name = str(ts.name) + + if options.save_tiled_properties: + set_tiled_properties_as_meta(result, ts) + if options.custom_properties: + if "properties" in ts and "propertytypes" in ts: + set_custom_properties(result, ts) + + if options.custom_properties and options.tile_metadata: + result.set_meta("tile_meta", tile_meta) + + return result + +# Makes a standalone TileSet. Useful for importing TileSets from Tiled +# Returns an error code if fails +func build_tileset(source_path, options): + var set = read_tileset_file(source_path) + if typeof(set) == TYPE_INT: + return set + if typeof(set) != TYPE_DICTIONARY: + return ERR_INVALID_DATA + + # Just to validate and build correctly using the existing builder + set["firstgid"] = 0 + + return build_tileset_for_scene([set], source_path, options) + +# Loads an image from a given path +# Returns a Texture +func load_image(rel_path, source_path, options): + flags = options.image_flags if "image_flags" in options else Texture.FLAGS_DEFAULT + var embed = options.embed_internal_images if "embed_internal_images" in options else false + + var ext = rel_path.get_extension().to_lower() + if ext != "png" and ext != "jpg": + print_error("Unsupported image format: %s. Use PNG or JPG instead." % [ext]) + return ERR_FILE_UNRECOGNIZED + + var total_path = rel_path + if rel_path.is_rel_path(): + total_path = ProjectSettings.globalize_path(source_path.get_base_dir()).plus_file(rel_path) + total_path = ProjectSettings.localize_path(total_path) + + var dir = Directory.new() + if not dir.file_exists(total_path): + print_error("Image not found: %s" % [total_path]) + return ERR_FILE_NOT_FOUND + + if not total_path.begins_with("res://"): + # External images need to be embedded + embed = true + + var image = null + if embed: + var img = Image.new() + img.load(total_path) + image = ImageTexture.new() + image.create_from_image(img, flags) + else: + image = ResourceLoader.load(total_path, "ImageTexture") + if image != null: + image.set_flags(flags) + + return image + +# Reads a file and returns its contents as a dictionary +# Returns an error code if fails +func read_file(path): + if path.get_extension().to_lower() == "tmx": + var tmx_to_dict = TiledXMLToDictionary.new() + var data = tmx_to_dict.read_tmx(path) + if typeof(data) != TYPE_DICTIONARY: + # Error happened + print_error("Error parsing map file '%s'." % [path]) + # Return error or result + return data + + # Not TMX, must be JSON + var file = File.new() + var err = file.open(path, File.READ) + if err != OK: + return err + + var content = JSON.parse(file.get_as_text()) + if content.error != OK: + print_error("Error parsing JSON: " + content.error_string) + return content.error + + return content.result + +# Reads a tileset file and return its contents as a dictionary +# Returns an error code if fails +func read_tileset_file(path): + if path.get_extension().to_lower() == "tsx": + var tmx_to_dict = TiledXMLToDictionary.new() + var data = tmx_to_dict.read_tsx(path) + if typeof(data) != TYPE_DICTIONARY: + # Error happened + print_error("Error parsing map file '%s'." % [path]) + # Return error or result + return data + + # Not TSX, must be JSON + var file = File.new() + var err = file.open(path, File.READ) + if err != OK: + return err + + var content = JSON.parse(file.get_as_text()) + if content.error != OK: + print_error("Error parsing JSON: " + content.error_string) + return content.error + + return content.result + +# Creates a shape from an object data +# Returns a valid shape depending on the object type (collision/occluder/navigation) +func shape_from_object(object): + var shape = ERR_INVALID_DATA + set_default_obj_params(object) + + if "polygon" in object or "polyline" in object: + var vertices = PoolVector2Array() + + if "polygon" in object: + for point in object.polygon: + vertices.push_back(Vector2(float(point.x), float(point.y))) + else: + for point in object.polyline: + vertices.push_back(Vector2(float(point.x), float(point.y))) + + if object.type == "navigation": + shape = NavigationPolygon.new() + shape.vertices = vertices + shape.add_outline(vertices) + shape.make_polygons_from_outlines() + elif object.type == "occluder": + shape = OccluderPolygon2D.new() + shape.polygon = vertices + shape.closed = "polygon" in object + else: + if is_convex(vertices): + var sorter = PolygonSorter.new() + vertices = sorter.sort_polygon(vertices) + shape = ConvexPolygonShape2D.new() + shape.points = vertices + else: + shape = ConcavePolygonShape2D.new() + var segments = [vertices[0]] + for x in range(1, vertices.size()): + segments.push_back(vertices[x]) + segments.push_back(vertices[x]) + segments.push_back(vertices[0]) + shape.segments = PoolVector2Array(segments) + + elif "ellipse" in object: + if object.type == "navigation" or object.type == "occluder": + print_error("Ellipse shapes are not supported as navigation or occluder. Use polygon/polyline instead.") + return ERR_INVALID_DATA + + if not "width" in object or not "height" in object: + print_error("Missing width or height in ellipse shape.") + return ERR_INVALID_DATA + + var w = abs(float(object.width)) + var h = abs(float(object.height)) + + if w == h: + shape = CircleShape2D.new() + shape.radius = w / 2.0 + else: + # Using a capsule since it's the closest from an ellipse + shape = CapsuleShape2D.new() + shape.radius = w / 2.0 + shape.height = h / 2.0 + + else: # Rectangle + if not "width" in object or not "height" in object: + print_error("Missing width or height in rectangle shape.") + return ERR_INVALID_DATA + + var size = Vector2(float(object.width), float(object.height)) + + if object.type == "navigation" or object.type == "occluder": + # Those types only accept polygons, so make one from the rectangle + var vertices = PoolVector2Array([ + Vector2(0, 0), + Vector2(size.x, 0), + size, + Vector2(0, size.y) + ]) + if object.type == "navigation": + shape = NavigationPolygon.new() + shape.vertices = vertices + shape.add_outline(vertices) + shape.make_polygons_from_outlines() + else: + shape = OccluderPolygon2D.new() + shape.polygon = vertices + else: + shape = RectangleShape2D.new() + shape.extents = size / 2.0 + + return shape + +# Determines if the set of vertices is convex or not +# Returns a boolean +func is_convex(vertices): + var size = vertices.size() + if size <= 3: + # Less than 3 verices can't be concave + return true + + var cp = 0 + + for i in range(0, size + 2): + var p1 = vertices[(i + 0) % size] + var p2 = vertices[(i + 1) % size] + var p3 = vertices[(i + 2) % size] + + var prev_cp = cp + cp = (p2.x - p1.x) * (p3.y - p2.y) - (p2.y - p1.y) * (p3.x - p2.x) + if i > 0 and sign(cp) != sign(prev_cp): + return false + + return true + +# Decompress the data of the layer +# Compression argument is a string, either "gzip", "zlib", or "zstd" +func decompress_layer_data(layer_data, compression, map_size): + var compression_type = -1 + match compression: + "zlib": + compression_type = File.COMPRESSION_DEFLATE + "gzip": + compression_type = File.COMPRESSION_GZIP + "zstd": + compression_type = File.COMPRESSION_ZSTD + _: + print_error("Unrecognized compression format: %s" % [compression]) + return ERR_INVALID_DATA + var expected_size = int(map_size.x) * int(map_size.y) * 4 + var raw_data = Marshalls.base64_to_raw(layer_data).decompress(expected_size, compression_type) + + return decode_layer(raw_data) + +# Reads the layer as a base64 data +# Returns an array of ints as the decoded layer would be +func read_base64_layer_data(layer_data): + var decoded = Marshalls.base64_to_raw(layer_data) + return decode_layer(decoded) + +# Reads a PoolByteArray and returns the layer array +# Used for base64 encoded and compressed layers +func decode_layer(layer_data): + var result = [] + for i in range(0, layer_data.size(), 4): + var num = (layer_data[i]) | \ + (layer_data[i + 1] << 8) | \ + (layer_data[i + 2] << 16) | \ + (layer_data[i + 3] << 24) + result.push_back(num) + return result + +# Set the custom properties into the metadata of the object +func set_custom_properties(object, tiled_object): + if not "properties" in tiled_object or not "propertytypes" in tiled_object: + return + + var properties = get_custom_properties(tiled_object.properties, tiled_object.propertytypes) + for property in properties: + object.set_meta(property, properties[property]) + +# Get the custom properties as a dictionary +# Useful for tile meta, which is not stored directly +func get_custom_properties(properties, types): + var result = {} + + for property in properties: + var value = null + if str(types[property]).to_lower() == "bool": + value = bool(properties[property]) + elif str(types[property]).to_lower() == "int": + value = int(properties[property]) + elif str(types[property]).to_lower() == "float": + value = float(properties[property]) + elif str(types[property]).to_lower() == "color": + value = Color(properties[property]) + else: + value = str(properties[property]) + result[property] = value + return result + +# Get the available whitelisted properties from the Tiled object +# And them as metadata in the Godot object +func set_tiled_properties_as_meta(object, tiled_object): + for property in whitelist_properties: + if property in tiled_object: + object.set_meta(property, tiled_object[property]) + +# Custom function to sort objects in an object layer +# This is done to support the "topdown" draw order, which sorts by 'y' coordinate +func object_sorter(first, second): + if first.y == second.y: + return first.id < second.id + return first.y < second.y + +# Create the tilecount for the TileSet if not present. +# Based on the image and tile dimensions. +func make_tilecount(tilesize, imagesize, margin, spacing): + var horizontal_tile_size = int(tilesize.x + margin * 2 + spacing) + var vertical_tile_size = int(tilesize.y + margin * 2 + spacing) + + var horizontal_tile_count = int(imagesize.x) / horizontal_tile_size; + var vertical_tile_count = int(imagesize.y) / vertical_tile_size; + + return horizontal_tile_count * vertical_tile_count + +# Validates the map dictionary content for missing or invalid keys +# Returns an error code +func validate_map(map): + if not "type" in map or map.type != "map": + print_error("Missing or invalid type property.") + return ERR_INVALID_DATA + elif not "version" in map or int(map.version) != 1: + print_error("Missing or invalid map version.") + return ERR_INVALID_DATA + elif not "tileheight" in map or not str(map.tileheight).is_valid_integer(): + print_error("Missing or invalid tileheight property.") + return ERR_INVALID_DATA + elif not "tilewidth" in map or not str(map.tilewidth).is_valid_integer(): + print_error("Missing or invalid tilewidth property.") + return ERR_INVALID_DATA + elif not "layers" in map or typeof(map.layers) != TYPE_ARRAY: + print_error("Missing or invalid layers property.") + return ERR_INVALID_DATA + elif not "tilesets" in map or typeof(map.tilesets) != TYPE_ARRAY: + print_error("Missing or invalid tilesets property.") + return ERR_INVALID_DATA + if "orientation" in map and (map.orientation == "staggered" or map.orientation == "hexagonal"): + if not "staggeraxis" in map: + print_error("Missing stagger axis property.") + return ERR_INVALID_DATA + elif not "staggerindex" in map: + print_error("Missing stagger axis property.") + return ERR_INVALID_DATA + return OK + +# Validates the tileset dictionary content for missing or invalid keys +# Returns an error code +func validate_tileset(tileset): + if not "firstgid" in tileset or not str(tileset.firstgid).is_valid_integer(): + print_error("Missing or invalid firstgid tileset property.") + return ERR_INVALID_DATA + elif not "tilewidth" in tileset or not str(tileset.tilewidth).is_valid_integer(): + print_error("Missing or invalid tilewidth tileset property.") + return ERR_INVALID_DATA + elif not "tileheight" in tileset or not str(tileset.tileheight).is_valid_integer(): + print_error("Missing or invalid tileheight tileset property.") + return ERR_INVALID_DATA + if not "image" in tileset: + for tile in tileset.tiles: + if not "image" in tileset.tiles[tile]: + print_error("Missing or invalid image in tileset property.") + return ERR_INVALID_DATA + elif not "imagewidth" in tileset.tiles[tile] or not str(tileset.tiles[tile].imagewidth).is_valid_integer(): + print_error("Missing or invalid imagewidth tileset property 1.") + return ERR_INVALID_DATA + elif not "imageheight" in tileset.tiles[tile] or not str(tileset.tiles[tile].imageheight).is_valid_integer(): + print_error("Missing or invalid imageheight tileset property.") + return ERR_INVALID_DATA + else: + if not "imagewidth" in tileset or not str(tileset.imagewidth).is_valid_integer(): + print_error("Missing or invalid imagewidth tileset property 2.") + return ERR_INVALID_DATA + elif not "imageheight" in tileset or not str(tileset.imageheight).is_valid_integer(): + print_error("Missing or invalid imageheight tileset property.") + return ERR_INVALID_DATA + return OK + +# Validates the layer dictionary content for missing or invalid keys +# Returns an error code +func validate_layer(layer): + if not "type" in layer: + print_error("Missing or invalid type layer property.") + return ERR_INVALID_DATA + elif not "name" in layer: + print_error("Missing or invalid name layer property.") + return ERR_INVALID_DATA + match layer.type: + "tilelayer": + if not "height" in layer or not str(layer.height).is_valid_integer(): + print_error("Missing or invalid layer height property.") + return ERR_INVALID_DATA + elif not "width" in layer or not str(layer.width).is_valid_integer(): + print_error("Missing or invalid layer width property.") + return ERR_INVALID_DATA + elif not "data" in layer: + if not "chunks" in layer: + print_error("Missing data or chunks layer properties.") + return ERR_INVALID_DATA + elif typeof(layer.chunks) != TYPE_ARRAY: + print_error("Invalid chunks layer property.") + return ERR_INVALID_DATA + elif "encoding" in layer: + if layer.encoding == "base64" and typeof(layer.data) != TYPE_STRING: + print_error("Invalid data layer property.") + return ERR_INVALID_DATA + if layer.encoding != "base64" and typeof(layer.data) != TYPE_ARRAY: + print_error("Invalid data layer property.") + return ERR_INVALID_DATA + elif typeof(layer.data) != TYPE_ARRAY: + print_error("Invalid data layer property.") + return ERR_INVALID_DATA + if "compression" in layer: + if layer.compression != "gzip" and layer.compression != "zlib" and layer.compression != "zstd": + print_error("Invalid compression type.") + return ERR_INVALID_DATA + "imagelayer": + if not "image" in layer or typeof(layer.image) != TYPE_STRING: + print_error("Missing or invalid image path for layer.") + return ERR_INVALID_DATA + "objectgroup": + if not "objects" in layer or typeof(layer.objects) != TYPE_ARRAY: + print_error("Missing or invalid objects array for layer.") + return ERR_INVALID_DATA + "group": + if not "layers" in layer or typeof(layer.layers) != TYPE_ARRAY: + print_error("Missing or invalid layer array for group layer.") + return ERR_INVALID_DATA + return OK + +func validate_chunk(chunk): + if not "data" in chunk: + print_error("Missing data chunk property.") + return ERR_INVALID_DATA + elif not "height" in chunk or not str(chunk.height).is_valid_integer(): + print_error("Missing or invalid height chunk property.") + return ERR_INVALID_DATA + elif not "width" in chunk or not str(chunk.width).is_valid_integer(): + print_error("Missing or invalid width chunk property.") + return ERR_INVALID_DATA + elif not "x" in chunk or not str(chunk.x).is_valid_integer(): + print_error("Missing or invalid x chunk property.") + return ERR_INVALID_DATA + elif not "y" in chunk or not str(chunk.y).is_valid_integer(): + print_error("Missing or invalid y chunk property.") + return ERR_INVALID_DATA + return OK + +# Custom function to print error, to centralize the prefix addition +func print_error(err): + printerr(error_prefix + err) + +func get_template(path): + # If this template has not yet been loaded + if not _loaded_templates.has(path): + # IS XML + if path.get_extension().to_lower() == "tx": + var parser = XMLParser.new() + var err = parser.open(path) + if err != OK: + print_error("Error opening TX file '%s'." % [path]) + return err + var content = parse_template(parser, path) + if typeof(content) != TYPE_DICTIONARY: + # Error happened + print_error("Error parsing template map file '%s'." % [path]) + return false + _loaded_templates[path] = content + + # IS JSON + else: + var file = File.new() + var err = file.open(path, File.READ) + if err != OK: + return err + + var json_res = JSON.parse(file.get_as_text()) + if json_res.error != OK: + print_error("Error parsing JSON template map file '%s'." % [path]) + return json_res.error + + var result = json_res.result + if typeof(result) != TYPE_DICTIONARY: + print_error("Error parsing JSON template map file '%s'." % [path]) + return ERR_INVALID_DATA + + var object = result.object + if object.has("gid"): + if result.has("tileset"): + var ts_path = remove_filename_from_path(path) + result.tileset.source + var tileset_gid_increment = get_first_gid_from_tileset_path(ts_path) - 1 + object.gid += tileset_gid_increment + + _loaded_templates[path] = object + + var dict = _loaded_templates[path] + var dictCopy = {} + for k in dict: + dictCopy[k] = dict[k] + + return dictCopy + +func parse_template(parser, path): + var err = OK + # Template root node shouldn't have attributes + var data = {} + var tileset_gid_increment = 0 + data.id = 0 + + err = parser.read() + while err == OK: + if parser.get_node_type() == XMLParser.NODE_ELEMENT_END: + if parser.get_node_name() == "template": + break + + elif parser.get_node_type() == XMLParser.NODE_ELEMENT: + if parser.get_node_name() == "tileset": + var ts_path = remove_filename_from_path(path) + parser.get_named_attribute_value_safe("source") + tileset_gid_increment = get_first_gid_from_tileset_path(ts_path) - 1 + data.tileset = ts_path + + if parser.get_node_name() == "object": + var object = TiledXMLToDictionary.parse_object(parser) + for k in object: + data[k] = object[k] + + err = parser.read() + + if data.has("gid"): + data["gid"] += tileset_gid_increment + + return data + +func get_first_gid_from_tileset_path(path): + for t in _tileset_path_to_first_gid: + if is_same_file(path, t): + return _tileset_path_to_first_gid[t] + + return 0 + +static func get_filename_from_path(path): + var substrings = path.split("/", false) + var file_name = substrings[substrings.size() - 1] + return file_name + +static func remove_filename_from_path(path): + var file_name = get_filename_from_path(path) + var stringSize = path.length() - file_name.length() + var file_path = path.substr(0,stringSize) + return file_path + +static func is_same_file(path1, path2): + var file1 = File.new() + var err = file1.open(path1, File.READ) + if err != OK: + return err + + var file2 = File.new() + err = file2.open(path2, File.READ) + if err != OK: + return err + + var file1_str = file1.get_as_text() + var file2_str = file2.get_as_text() + + if file1_str == file2_str: + return true + + return false + +static func apply_template(object, template_immutable): + for k in template_immutable: + # Do not overwrite any object data + if typeof(template_immutable[k]) == TYPE_DICTIONARY: + if not object.has(k): + object[k] = {} + apply_template(object[k], template_immutable[k]) + + elif not object.has(k): + object[k] = template_immutable[k] diff --git a/addons/vnen.tiled_importer/tiled_tileset_import_plugin.gd b/addons/vnen.tiled_importer/tiled_tileset_import_plugin.gd new file mode 100644 index 0000000..9a86758 --- /dev/null +++ b/addons/vnen.tiled_importer/tiled_tileset_import_plugin.gd @@ -0,0 +1,121 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 George Marques +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +tool +extends EditorImportPlugin + +enum { PRESET_DEFAULT, PRESET_PIXEL_ART } + +const TiledMapReader = preload("tiled_map_reader.gd") + +func get_importer_name(): + return "vnen.tiled_tileset_importer" + +func get_visible_name(): + return "TileSet from Tiled" + +func get_recognized_extensions(): + if ProjectSettings.get_setting("tiled_importer/enable_json_format"): + return ["json", "tsx"] + else: + return ["tsx"] + +func get_save_extension(): + return "res" + +func get_import_order(): + return 100 + +func get_resource_type(): + return "TileSet" + +func get_preset_count(): + return 2 + +func get_preset_name(preset): + match preset: + PRESET_DEFAULT: return "Default" + PRESET_PIXEL_ART: return "Pixel Art" + +func get_import_options(preset): + return [ + { + "name": "custom_properties", + "default_value": true + }, + { + "name": "tile_metadata", + "default_value": false + }, + { + "name": "image_flags", + "default_value": 0 if preset == PRESET_PIXEL_ART else Texture.FLAGS_DEFAULT, + "property_hint": PROPERTY_HINT_FLAGS, + "hint_string": "Mipmaps,Repeat,Filter,Anisotropic,sRGB,Mirrored Repeat" + }, + { + "name": "embed_internal_images", + "default_value": true if preset == PRESET_PIXEL_ART else false + }, + { + "name": "save_tiled_properties", + "default_value": false + }, + { + "name": "post_import_script", + "default_value": "", + "property_hint": PROPERTY_HINT_FILE, + "hint_string": "*.gd;GDScript" + } + ] + +func get_option_visibility(option, options): + return true + +func import(source_file, save_path, options, r_platform_variants, r_gen_files): + var map_reader = TiledMapReader.new() + + var tileset = map_reader.build_tileset(source_file, options) + + if typeof(tileset) != TYPE_OBJECT: + # Error happened + return tileset + + # Post imports script + if not options.post_import_script.empty(): + var script = load(options.post_import_script) + if not script or not script is GDScript: + printerr("Post import script is not a GDScript.") + return ERR_INVALID_PARAMETER + + script = script.new() + if not script.has_method("post_import"): + printerr("Post import script does not have a 'post_import' method.") + return ERR_INVALID_PARAMETER + + tileset = script.post_import(tileset) + + if not tileset or not tileset is TileSet: + printerr("Invalid TileSet returned from post import script.") + return ERR_INVALID_DATA + + return ResourceSaver.save("%s.%s" % [save_path, get_save_extension()], tileset) diff --git a/addons/vnen.tiled_importer/tiled_xml_to_dict.gd b/addons/vnen.tiled_importer/tiled_xml_to_dict.gd new file mode 100644 index 0000000..9064b90 --- /dev/null +++ b/addons/vnen.tiled_importer/tiled_xml_to_dict.gd @@ -0,0 +1,571 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 George Marques +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +tool +extends Reference + +# Reads a TMX file from a path and return a Dictionary with the same structure +# as the JSON map format +# Returns an error code if failed +func read_tmx(path): + var parser = XMLParser.new() + var err = parser.open(path) + if err != OK: + printerr("Error opening TMX file '%s'." % [path]) + return err + + while parser.get_node_type() != XMLParser.NODE_ELEMENT: + err = parser.read() + if err != OK: + printerr("Error parsing TMX file '%s' (around line %d)." % [path, parser.get_current_line()]) + return err + + if parser.get_node_name().to_lower() != "map": + printerr("Error parsing TMX file '%s'. Expected 'map' element.") + return ERR_INVALID_DATA + + var data = attributes_to_dict(parser) + if not "infinite" in data: + data.infinite = false + data.type = "map" + data.tilesets = [] + data.layers = [] + + err = parser.read() + if err != OK: + printerr("Error parsing TMX file '%s' (around line %d)." % [path, parser.get_current_line()]) + return err + + while err == OK: + if parser.get_node_type() == XMLParser.NODE_ELEMENT_END: + if parser.get_node_name() == "map": + break + elif parser.get_node_type() == XMLParser.NODE_ELEMENT: + if parser.get_node_name() == "tileset": + # Empty element means external tileset + if not parser.is_empty(): + var tileset = parse_tileset(parser) + if typeof(tileset) != TYPE_DICTIONARY: + # Error happened + return err + data.tilesets.push_back(tileset) + else: + var tileset_data = attributes_to_dict(parser) + if not "source" in tileset_data: + printerr("Error parsing TMX file '%s'. Missing tileset source (around line %d)." % [path, parser.get_current_line()]) + return ERR_INVALID_DATA + data.tilesets.push_back(tileset_data) + + elif parser.get_node_name() == "layer": + var layer = parse_tile_layer(parser, data.infinite) + if typeof(layer) != TYPE_DICTIONARY: + printerr("Error parsing TMX file '%s'. Invalid tile layer data (around line %d)." % [path, parser.get_current_line()]) + return ERR_INVALID_DATA + data.layers.push_back(layer) + + elif parser.get_node_name() == "imagelayer": + var layer = parse_image_layer(parser) + if typeof(layer) != TYPE_DICTIONARY: + printerr("Error parsing TMX file '%s'. Invalid image layer data (around line %d)." % [path, parser.get_current_line()]) + return ERR_INVALID_DATA + data.layers.push_back(layer) + + elif parser.get_node_name() == "objectgroup": + var layer = parse_object_layer(parser) + if typeof(layer) != TYPE_DICTIONARY: + printerr("Error parsing TMX file '%s'. Invalid object layer data (around line %d)." % [path, parser.get_current_line()]) + return ERR_INVALID_DATA + data.layers.push_back(layer) + + elif parser.get_node_name() == "group": + var layer = parse_group_layer(parser, data.infinite) + if typeof(layer) != TYPE_DICTIONARY: + printerr("Error parsing TMX file '%s'. Invalid group layer data (around line %d)." % [path, parser.get_current_line()]) + return ERR_INVALID_DATA + data.layers.push_back(layer) + + elif parser.get_node_name() == "properties": + var prop_data = parse_properties(parser) + if typeof(prop_data) == TYPE_STRING: + return prop_data + + data.properties = prop_data.properties + data.propertytypes = prop_data.propertytypes + + err = parser.read() + + return data + +# Reads a TSX and return a tileset dictionary +# Returns an error code if fails +func read_tsx(path): + var parser = XMLParser.new() + var err = parser.open(path) + if err != OK: + printerr("Error opening TSX file '%s'." % [path]) + return err + + while parser.get_node_type() != XMLParser.NODE_ELEMENT: + err = parser.read() + if err != OK: + printerr("Error parsing TSX file '%s' (around line %d)." % [path, parser.get_current_line()]) + return err + + if parser.get_node_name().to_lower() != "tileset": + printerr("Error parsing TMX file '%s'. Expected 'map' element.") + return ERR_INVALID_DATA + + var tileset = parse_tileset(parser) + + return tileset + +# Parses a tileset element from the XML and return a dictionary +# Return an error code if fails +func parse_tileset(parser): + var err = OK + var data = attributes_to_dict(parser) + data.tiles = {} + + err = parser.read() + while err == OK: + if parser.get_node_type() == XMLParser.NODE_ELEMENT_END: + if parser.get_node_name() == "tileset": + break + + elif parser.get_node_type() == XMLParser.NODE_ELEMENT: + if parser.get_node_name() == "tile": + var attr = attributes_to_dict(parser) + var tile_data = parse_tile_data(parser) + if typeof(tile_data) != TYPE_DICTIONARY: + # Error happened + return tile_data + if "properties" in tile_data and "propertytypes" in tile_data: + if not "tileproperties" in data: + data.tileproperties = {} + data.tilepropertytypes = {} + data.tileproperties[str(attr.id)] = tile_data.properties + data.tilepropertytypes[str(attr.id)] = tile_data.propertytypes + tile_data.erase("tileproperties") + tile_data.erase("tilepropertytypes") + data.tiles[str(attr.id)] = tile_data + + elif parser.get_node_name() == "image": + var attr = attributes_to_dict(parser) + if not "source" in attr: + printerr("Error loading image tag. No source attribute found (around line %d)." % [parser.get_current_line()]) + return ERR_INVALID_DATA + data.image = attr.source + if "width" in attr: + data.imagewidth = attr.width + if "height" in attr: + data.imageheight = attr.height + + elif parser.get_node_name() == "properties": + var prop_data = parse_properties(parser) + if typeof(prop_data) != TYPE_DICTIONARY: + # Error happened + return prop_data + + data.properties = prop_data.properties + data.propertytypes = prop_data.propertytypes + + err = parser.read() + + return data + + +# Parses the data of a single tile from the XML and return a dictionary +# Returns an error code if fails +func parse_tile_data(parser): + var err = OK + var data = {} + var obj_group = {} + if parser.is_empty(): + return data + + err = parser.read() + while err == OK: + + if parser.get_node_type() == XMLParser.NODE_ELEMENT_END: + if parser.get_node_name() == "tile": + return data + elif parser.get_node_name() == "objectgroup": + data.objectgroup = obj_group + + elif parser.get_node_type() == XMLParser.NODE_ELEMENT: + if parser.get_node_name() == "image": + # If there are multiple images in one tile we only use the last one. + var attr = attributes_to_dict(parser) + if not "source" in attr: + printerr("Error loading image tag. No source attribute found (around line %d)." % [parser.get_current_line()]) + return ERR_INVALID_DATA + data.image = attr.source + data.imagewidth = attr.width + data.imageheight = attr.height + + elif parser.get_node_name() == "objectgroup": + obj_group = attributes_to_dict(parser) + for attr in ["width", "height", "offsetx", "offsety"]: + if not attr in obj_group: + data[attr] = 0 + if not "opacity" in data: + data.opacity = 1 + if not "visible" in data: + data.visible = true + if parser.is_empty(): + data.objectgroup = obj_group + + elif parser.get_node_name() == "object": + if not "objects" in obj_group: + obj_group.objects = [] + var obj = parse_object(parser) + if typeof(obj) != TYPE_DICTIONARY: + # Error happened + return obj + obj_group.objects.push_back(obj) + + elif parser.get_node_name() == "properties": + var prop_data = parse_properties(parser) + data["properties"] = prop_data.properties + data["propertytypes"] = prop_data.propertytypes + + elif parser.get_node_name() == "animation": + var frame_list = [] + var err2 = parser.read() + while err2 == OK: + if parser.get_node_type() == XMLParser.NODE_ELEMENT: + if parser.get_node_name() == "frame": + var frame = {"tileid": 0, "duration": 0} + for i in parser.get_attribute_count(): + if parser.get_attribute_name(i) == "tileid": + frame["tileid"] = parser.get_attribute_value(i) + if parser.get_attribute_name(i) == "duration": + frame["duration"] = parser.get_attribute_value(i) + frame_list.push_back(frame) + elif parser.get_node_type() == XMLParser.NODE_ELEMENT_END: + if parser.get_node_name() == "animation": + break + err2 = parser.read() + + data["animation"] = frame_list + + err = parser.read() + + return data + +# Parses the data of a single object from the XML and return a dictionary +# Returns an error code if fails +static func parse_object(parser): + var err = OK + var data = attributes_to_dict(parser) + + if not parser.is_empty(): + err = parser.read() + while err == OK: + if parser.get_node_type() == XMLParser.NODE_ELEMENT_END: + if parser.get_node_name() == "object": + break + + elif parser.get_node_type() == XMLParser.NODE_ELEMENT: + if parser.get_node_name() == "properties": + var prop_data = parse_properties(parser) + data["properties"] = prop_data.properties + data["propertytypes"] = prop_data.propertytypes + + elif parser.get_node_name() == "point": + data.point = true + + elif parser.get_node_name() == "ellipse": + data.ellipse = true + + elif parser.get_node_name() == "polygon" or parser.get_node_name() == "polyline": + var points = [] + var points_raw = parser.get_named_attribute_value("points").split(" ", false, 0) + + for pr in points_raw: + points.push_back({ + "x": float(pr.split(",")[0]), + "y": float(pr.split(",")[1]), + }) + + data[parser.get_node_name()] = points + + err = parser.read() + + return data + + +# Parses a tile layer from the XML and return a dictionary +# Returns an error code if fails +func parse_tile_layer(parser, infinite): + var err = OK + var data = attributes_to_dict(parser) + data.type = "tilelayer" + if not "x" in data: + data.x = 0 + if not "y" in data: + data.y = 0 + if infinite: + data.chunks = [] + else: + data.data = [] + + var current_chunk = null + var encoding = "" + + if not parser.is_empty(): + err = parser.read() + + while err == OK: + if parser.get_node_type() == XMLParser.NODE_ELEMENT_END: + if parser.get_node_name() == "layer": + break + elif parser.get_node_name() == "chunk": + data.chunks.push_back(current_chunk) + current_chunk = null + + elif parser.get_node_type() == XMLParser.NODE_ELEMENT: + if parser.get_node_name() == "data": + var attr = attributes_to_dict(parser) + + if "compression" in attr: + data.compression = attr.compression + + if "encoding" in attr: + encoding = attr.encoding + if attr.encoding != "csv": + data.encoding = attr.encoding + + if not infinite: + err = parser.read() + if err != OK: + return err + + if attr.encoding != "csv": + data.data = parser.get_node_data().strip_edges() + else: + var csv = parser.get_node_data().split(",", false) + + for v in csv: + data.data.push_back(int(v.strip_edges())) + + elif parser.get_node_name() == "tile": + var gid = int(parser.get_named_attribute_value_safe("gid")) + if infinite: + current_chunk.data.push_back(gid) + else: + data.data.push_back(gid) + + elif parser.get_node_name() == "chunk": + current_chunk = attributes_to_dict(parser) + current_chunk.data = [] + if encoding != "": + err = parser.read() + if err != OK: + return err + if encoding != "csv": + current_chunk.data = parser.get_node_data().strip_edges() + else: + var csv = parser.get_node_data().split(",", false) + for v in csv: + current_chunk.data.push_back(int(v.strip_edges())) + + elif parser.get_node_name() == "properties": + var prop_data = parse_properties(parser) + if typeof(prop_data) == TYPE_STRING: + return prop_data + + data.properties = prop_data.properties + data.propertytypes = prop_data.propertytypes + + err = parser.read() + + return data + +# Parses an object layer from the XML and return a dictionary +# Returns an error code if fails +func parse_object_layer(parser): + var err = OK + var data = attributes_to_dict(parser) + data.type = "objectgroup" + data.objects = [] + + if not parser.is_empty(): + err = parser.read() + while err == OK: + if parser.get_node_type() == XMLParser.NODE_ELEMENT_END: + if parser.get_node_name() == "objectgroup": + break + if parser.get_node_type() == XMLParser.NODE_ELEMENT: + if parser.get_node_name() == "object": + data.objects.push_back(parse_object(parser)) + elif parser.get_node_name() == "properties": + var prop_data = parse_properties(parser) + if typeof(prop_data) != TYPE_DICTIONARY: + # Error happened + return prop_data + data.properties = prop_data.properties + data.propertytypes = prop_data.propertytypes + + err = parser.read() + + return data + +# Parses an image layer from the XML and return a dictionary +# Returns an error code if fails +func parse_image_layer(parser): + var err = OK + var data = attributes_to_dict(parser) + data.type = "imagelayer" + data.image = "" + + if not parser.is_empty(): + err = parser.read() + + while err == OK: + if parser.get_node_type() == XMLParser.NODE_ELEMENT_END: + if parser.get_node_name().to_lower() == "imagelayer": + break + elif parser.get_node_type() == XMLParser.NODE_ELEMENT: + if parser.get_node_name().to_lower() == "image": + var image = attributes_to_dict(parser) + if not image.has("source"): + printerr("Missing source attribute in imagelayer (around line %d)." % [parser.get_current_line()]) + return ERR_INVALID_DATA + data.image = image.source + + elif parser.get_node_name() == "properties": + var prop_data = parse_properties(parser) + if typeof(prop_data) != TYPE_DICTIONARY: + # Error happened + return prop_data + data.properties = prop_data.properties + data.propertytypes = prop_data.propertytypes + + err = parser.read() + + return data + +# Parses a group layer from the XML and return a dictionary +# Returns an error code if fails +func parse_group_layer(parser, infinite): + var err = OK + var result = attributes_to_dict(parser) + result.type = "group" + result.layers = [] + + if not parser.is_empty(): + err = parser.read() + + while err == OK: + if parser.get_node_type() == XMLParser.NODE_ELEMENT_END: + if parser.get_node_name().to_lower() == "group": + break + elif parser.get_node_type() == XMLParser.NODE_ELEMENT: + if parser.get_node_name() == "layer": + var layer = parse_tile_layer(parser, infinite) + if typeof(layer) != TYPE_DICTIONARY: + printerr("Error parsing TMX file. Invalid tile layer data (around line %d)." % [parser.get_current_line()]) + return ERR_INVALID_DATA + result.layers.push_back(layer) + + elif parser.get_node_name() == "imagelayer": + var layer = parse_image_layer(parser) + if typeof(layer) != TYPE_DICTIONARY: + printerr("Error parsing TMX file. Invalid image layer data (around line %d)." % [parser.get_current_line()]) + return ERR_INVALID_DATA + result.layers.push_back(layer) + + elif parser.get_node_name() == "objectgroup": + var layer = parse_object_layer(parser) + if typeof(layer) != TYPE_DICTIONARY: + printerr("Error parsing TMX file. Invalid object layer data (around line %d)." % [parser.get_current_line()]) + return ERR_INVALID_DATA + result.layers.push_back(layer) + + elif parser.get_node_name() == "group": + var layer = parse_group_layer(parser, infinite) + if typeof(layer) != TYPE_DICTIONARY: + printerr("Error parsing TMX file. Invalid group layer data (around line %d)." % [parser.get_current_line()]) + return ERR_INVALID_DATA + result.layers.push_back(layer) + + elif parser.get_node_name() == "properties": + var prop_data = parse_properties(parser) + if typeof(prop_data) == TYPE_STRING: + return prop_data + + result.properties = prop_data.properties + result.propertytypes = prop_data.propertytypes + + err = parser.read() + return result + +# Parses properties data from the XML and return a dictionary +# Returns an error code if fails +static func parse_properties(parser): + var err = OK + var data = { + "properties": {}, + "propertytypes": {}, + } + + if not parser.is_empty(): + err = parser.read() + + while err == OK: + if parser.get_node_type() == XMLParser.NODE_ELEMENT_END: + if parser.get_node_name() == "properties": + break + elif parser.get_node_type() == XMLParser.NODE_ELEMENT: + if parser.get_node_name() == "property": + var prop_data = attributes_to_dict(parser) + if not (prop_data.has("name") and prop_data.has("value")): + printerr("Missing information in custom properties (around line %d)." % [parser.get_current_line()]) + return ERR_INVALID_DATA + + data.properties[prop_data.name] = prop_data.value + if prop_data.has("type"): + data.propertytypes[prop_data.name] = prop_data.type + else: + data.propertytypes[prop_data.name] = "string" + + err = parser.read() + + return data + +# Reads the attributes of the current element and return them as a dictionary +static func attributes_to_dict(parser): + var data = {} + for i in range(parser.get_attribute_count()): + var attr = parser.get_attribute_name(i) + var val = parser.get_attribute_value(i) + if val.is_valid_integer(): + val = int(val) + elif val.is_valid_float(): + val = float(val) + elif val == "true": + val = true + elif val == "false": + val = false + data[attr] = val + return data diff --git a/addons/vnen.tiled_importer/vnen.tiled_importer.gd b/addons/vnen.tiled_importer/vnen.tiled_importer.gd new file mode 100644 index 0000000..6f70315 --- /dev/null +++ b/addons/vnen.tiled_importer/vnen.tiled_importer.gd @@ -0,0 +1,45 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 George Marques +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +tool +extends EditorPlugin + +var import_plugin = null +var tileset_import_plugin = null + +func get_name(): + return "Tiled Map Importer" + +func _enter_tree(): + if not ProjectSettings.has_setting("tiled_importer/enable_json_format"): + ProjectSettings.set_setting("tiled_importer/enable_json_format", true) + + import_plugin = preload("tiled_import_plugin.gd").new() + tileset_import_plugin = preload("tiled_tileset_import_plugin.gd").new() + add_import_plugin(import_plugin) + add_import_plugin(tileset_import_plugin) + +func _exit_tree(): + remove_import_plugin(import_plugin) + remove_import_plugin(tileset_import_plugin) + import_plugin = null + tileset_import_plugin = null