1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330 |
- # 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",
- "orientation",
- "probability",
- "spacing",
- "tilecount",
- "tiledversion",
- "tileheight",
- "tilewidth",
- "type",
- "version",
- "visible",
- "width",
- ]
- # 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
- 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 = true
- tilemap.cell_tile_origin = TileMap.TILE_ORIGIN_BOTTOM_LEFT
- tilemap.collision_layer = options.collision_layer
- 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)
- 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.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
- 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
- # 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 = int(ts.tilecount)
- var gid = firstgid
- var x = margin
- var y = margin
- var i = 0
- var column = 0
- while i < tilecount:
- var tilepos = Vector2(x, y)
- var region = Rect2(tilepos, tilesize)
- var rel_id = str(gid - firstgid)
- result.create_tile(gid)
- if has_global_image:
- result.tile_set_texture(gid, image)
- result.tile_set_region(gid, region)
- if options.apply_offset:
- result.tile_set_texture_offset(gid, Vector2(0, -tilesize.y))
- elif not rel_id in ts.tiles:
- gid += 1
- continue
- 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 options.apply_offset:
- result.tile_set_texture_offset(gid, Vector2(0, -image.get_height()))
- 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 options.apply_offset:
- offset += result.tile_get_texture_offset(gid)
- 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 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]
- gid += 1
- 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
- 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):
- var 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:
- image = ImageTexture.new()
- image.load(total_path)
- 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" or "zlib"
- func decompress_layer_data(layer_data, compression, map_size):
- if compression != "gzip" and compression != "zlib":
- print_error("Unrecognized compression format: %s" % [compression])
- return ERR_INVALID_DATA
- var compression_type = File.COMPRESSION_DEFLATE if compression == "zlib" else File.COMPRESSION_GZIP
- 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
- # 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
- elif not "tilecount" in tileset or not str(tileset.tilecount).is_valid_integer():
- print_error("Missing or invalid tilecount 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":
- 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]
|