TetrArcade/arcade/read_tiled_map.py

352 lines
13 KiB
Python

"""
Functions and classes for managing a map created in the "Tiled Map Editor"
"""
import xml.etree.ElementTree as ElementTree
import base64
import zlib
import gzip
from pathlib import Path
from arcade.isometric import isometric_grid_to_screen
from arcade import Sprite
from arcade import SpriteList
class TiledMap:
""" This class holds a tiled map, and tile set from the map. """
def __init__(self):
self.global_tile_set = {}
self.layers_int_data = {}
self.layers = {}
self.version = None
self.orientation = None
self.renderorder = None
self.width = None
self.height = None
self.tilewidth = None
self.tileheight = None
self.backgroundcolor = None
self.nextobjectid = None
class Tile:
""" This class represents an individual tile from a tileset. """
def __init__(self):
self.local_id = 0
self.width = 0
self.height = 0
self.source = None
self.points = None
class GridLocation:
""" This represents a location on the grid. Contains the x/y of the
grid location, and the tile that is on it. """
def __init__(self):
self.tile = None
self.center_x = 0
self.center_y = 0
def _process_csv_encoding(data_text):
layer_grid_ints = []
lines = data_text.split("\n")
for line in lines:
line_list = line.split(",")
while '' in line_list:
line_list.remove('')
line_list_int = [int(item) for item in line_list]
layer_grid_ints.append(line_list_int)
return layer_grid_ints
def _process_base64_encoding(data_text, compression, layer_width):
layer_grid_ints = [[]]
unencoded_data = base64.b64decode(data_text)
if compression == "zlib":
unzipped_data = zlib.decompress(unencoded_data)
elif compression == "gzip":
unzipped_data = gzip.decompress(unencoded_data)
elif compression is None:
unzipped_data = unencoded_data
else:
raise ValueError(f"Unsupported compression type '{compression}'.")
# Turn bytes into 4-byte integers
byte_count = 0
int_count = 0
int_value = 0
row_count = 0
for byte in unzipped_data:
int_value += byte << (byte_count * 8)
byte_count += 1
if byte_count % 4 == 0:
byte_count = 0
int_count += 1
layer_grid_ints[row_count].append(int_value)
int_value = 0
if int_count % layer_width == 0:
row_count += 1
layer_grid_ints.append([])
layer_grid_ints.pop()
return layer_grid_ints
def _parse_points(point_text: str):
result = []
point_list = point_text.split(" ")
for point in point_list:
z = point.split(",")
result.append([round(float(z[0])), round(float(z[1]))])
return result
def read_tiled_map(tmx_file: str, scaling: float = 1, tsx_file: str = None) -> TiledMap:
"""
read_tiled_map has been deprecated. Use arcade.tilemap.read_tmx instead.
Given a tmx_file, this will read in a tiled map, and return
a TiledMap object.
Given a tsx_file, the map will use it as the tileset.
If tsx_file is not specified, it will use the tileset specified
within the tmx_file.
Important: Tiles must be a "collection" of images.
Hitboxes can be drawn around tiles in the tileset editor,
but only polygons are supported.
(This is a great area for PR's to improve things.)
:param str tmx_file: String with name of our TMX file
:param float scaling: Scaling factor. 0.5 will half all widths and heights
:param str tsx_file: Tileset to use (can be specified in TMX file)
:returns: Map
:rtype: TiledMap
"""
from warnings import warn
warn('read_tiled_map has been deprecated. Use arcade.tilemap.read_tmx instead.', DeprecationWarning)
# Create a map object to store this stuff in
my_map = TiledMap()
# Read in and parse the file
tree = ElementTree.parse(tmx_file)
# Root node should be 'map'
map_tag = tree.getroot()
# Pull attributes that should be in the file for the map
my_map.version = map_tag.attrib["version"]
my_map.orientation = map_tag.attrib["orientation"]
my_map.renderorder = map_tag.attrib["renderorder"]
my_map.width = int(map_tag.attrib["width"])
my_map.height = int(map_tag.attrib["height"])
my_map.tilewidth = int(map_tag.attrib["tilewidth"])
my_map.tileheight = int(map_tag.attrib["tileheight"])
# Background color is optional, and may or may not be in there
if "backgroundcolor" in map_tag.attrib:
# Decode the background color string
background_color_string = map_tag.attrib["backgroundcolor"]
red_hex = "0x" + background_color_string[1:3]
green_hex = "0x" + background_color_string[3:5]
blue_hex = "0x" + background_color_string[5:7]
red = int(red_hex, 16)
green = int(green_hex, 16)
blue = int(blue_hex, 16)
my_map.backgroundcolor = (red, green, blue)
my_map.nextobjectid = map_tag.attrib["nextobjectid"]
# Grab all the tilesets
tileset_tag_list = map_tag.findall('./tileset')
# --- Tileset Data ---
# Loop through each tileset
for tileset_tag in tileset_tag_list:
firstgid = int(tileset_tag.attrib["firstgid"])
if tsx_file is not None or "source" in tileset_tag.attrib:
if tsx_file is not None:
tileset_tree = ElementTree.parse(tsx_file)
else:
source = tileset_tag.attrib["source"]
try:
tileset_tree = ElementTree.parse(source)
except FileNotFoundError:
source = Path(tmx_file).parent / Path(source)
tileset_tree = ElementTree.parse(source)
# Root node should be 'map'
tileset_root = tileset_tree.getroot()
tile_tag_list = tileset_root.findall("tile")
else:
# Grab each tile
tile_tag_list = tileset_tag.findall("tile")
# Loop through each tile
for tile_tag in tile_tag_list:
# Make a tile object
my_tile = Tile()
image = tile_tag.find("image")
my_tile.local_id = tile_tag.attrib["id"]
my_tile.width = int(image.attrib["width"])
my_tile.height = int(image.attrib["height"])
my_tile.source = image.attrib["source"]
key = str(int(my_tile.local_id) + 1)
my_map.global_tile_set[key] = my_tile
firstgid += 1
objectgroup = tile_tag.find("objectgroup")
if objectgroup:
my_object = objectgroup.find("object")
if my_object:
offset_x = round(float(my_object.attrib['x']))
offset_y = round(float(my_object.attrib['y']))
polygon = my_object.find("polygon")
if polygon is not None:
point_list = _parse_points(polygon.attrib['points'])
for point in point_list:
point[0] += offset_x
point[1] += offset_y
point[1] = my_tile.height - point[1]
point[0] -= my_tile.width // 2
point[1] -= my_tile.height // 2
point[0] *= scaling
point[1] *= scaling
point[0] = int(point[0])
point[1] = int(point[1])
my_tile.points = point_list
polygon = my_object.find("polyline")
if polygon is not None:
point_list = _parse_points(polygon.attrib['points'])
for point in point_list:
point[0] += offset_x
point[1] += offset_y
point[1] = my_tile.height - point[1]
point[0] -= my_tile.width // 2
point[1] -= my_tile.height // 2
point[0] *= scaling
point[1] *= scaling
point[0] = int(point[0])
point[1] = int(point[1])
if point_list[0][0] != point_list[-1][0] or point_list[0][1] != point_list[-1][1]:
point_list.append([point_list[0][0], point_list[0][1]])
my_tile.points = point_list
# --- Map Data ---
# Grab each layer
layer_tag_list = map_tag.findall('./layer')
for layer_tag in layer_tag_list:
layer_width = int(layer_tag.attrib['width'])
# Unzip and unencode each layer
data = layer_tag.find("data")
data_text = data.text.strip()
encoding = data.attrib['encoding']
if 'compression' in data.attrib:
compression = data.attrib['compression']
else:
compression = None
if encoding == "csv":
layer_grid_ints = _process_csv_encoding(data_text)
elif encoding == "base64":
layer_grid_ints = _process_base64_encoding(data_text, compression, layer_width)
else:
print(f"Error, unexpected encoding: {encoding}.")
break
# Great, we have a grid of ints. Save that according to the layer name
my_map.layers_int_data[layer_tag.attrib["name"]] = layer_grid_ints
# Now create grid objects for each tile
layer_grid_objs = []
for row_index, row in enumerate(layer_grid_ints):
layer_grid_objs.append([])
for column_index, column in enumerate(row):
grid_loc = GridLocation()
if layer_grid_ints[row_index][column_index] != 0:
key = str(layer_grid_ints[row_index][column_index])
if key not in my_map.global_tile_set:
print(f"Warning, tried to load '{key}' and it is not in the tileset.")
else:
grid_loc.tile = my_map.global_tile_set[key]
if my_map.renderorder == "right-down":
adjusted_row_index = my_map.height - row_index - 1
else:
adjusted_row_index = row_index
if my_map.orientation == "orthogonal":
grid_loc.center_x = column_index * my_map.tilewidth + my_map.tilewidth // 2
grid_loc.center_y = adjusted_row_index * my_map.tileheight + my_map.tilewidth // 2
else:
grid_loc.center_x, grid_loc.center_y = isometric_grid_to_screen(column_index,
row_index,
my_map.width,
my_map.height,
my_map.tilewidth,
my_map.tileheight)
layer_grid_objs[row_index].append(grid_loc)
my_map.layers[layer_tag.attrib["name"]] = layer_grid_objs
return my_map
def generate_sprites(map_object: TiledMap, layer_name: str, scaling: float, base_directory="") -> SpriteList:
"""
generate_sprites has been deprecated. Use arcade.tilemap.process_layer instead.
Generate the sprites for a layer in a map.
:param TiledMap map_object: Map previously read in from read_tiled_map function
:param layer_name: Name of the layer we want to generate sprites from. Case sensitive.
:param scaling: Scaling factor.
:param base_directory: Directory to read images from. Defaults to current directory.
:return: List of sprites
:rtype: SpriteList
"""
from warnings import warn
sprite_list = SpriteList()
if layer_name not in map_object.layers_int_data:
print(f"Warning, no layer named '{layer_name}'.")
return sprite_list
map_array = map_object.layers_int_data[layer_name]
# Loop through the layer and add in the wall list
for row_index, row in enumerate(map_array):
for column_index, item in enumerate(row):
if str(item) in map_object.global_tile_set:
tile_info = map_object.global_tile_set[str(item)]
tmx_file = base_directory + tile_info.source
my_sprite = Sprite(tmx_file, scaling)
my_sprite.right = column_index * (map_object.tilewidth * scaling)
my_sprite.top = (map_object.height - row_index) * (map_object.tileheight * scaling)
if tile_info.points is not None:
my_sprite.set_points(tile_info.points)
sprite_list.append(my_sprite)
elif item != 0:
print(f"Warning, could not find {item} image to load.")
return sprite_list