294 lines
9.8 KiB
Python
294 lines
9.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Script para migrar tilemaps de archivos TMX (Tiled) a archivos YAML.
|
|
|
|
Uso:
|
|
python tools/tmx_to_yaml.py
|
|
|
|
Busca todos los archivos .tmx en data/room/ y actualiza el tilemap
|
|
en el archivo .yaml correspondiente.
|
|
"""
|
|
|
|
import xml.etree.ElementTree as ET
|
|
from pathlib import Path
|
|
import re
|
|
|
|
|
|
def parse_layer_csv(data_element, width: int, height: int, offset: int) -> list[list[int]]:
|
|
"""
|
|
Parsea los datos CSV de una capa y convierte los indices.
|
|
|
|
Args:
|
|
data_element: Elemento XML con los datos CSV
|
|
width: Ancho del mapa en tiles
|
|
height: Alto del mapa en tiles
|
|
offset: Valor a restar de los indices (excepto 0 que pasa a -1)
|
|
|
|
Returns:
|
|
Lista de filas, cada fila es una lista de indices
|
|
"""
|
|
encoding = data_element.attrib.get('encoding', '')
|
|
if encoding != 'csv':
|
|
raise ValueError(f"Encoding no soportado: {encoding}. Solo se soporta CSV.")
|
|
|
|
csv_text = data_element.text.strip()
|
|
all_values = []
|
|
for val in csv_text.replace('\n', ',').split(','):
|
|
val = val.strip()
|
|
if val:
|
|
all_values.append(int(val))
|
|
|
|
# Convertir indices: 0 -> -1, N -> N - offset
|
|
result = []
|
|
for y in range(height):
|
|
row = []
|
|
for x in range(width):
|
|
idx = y * width + x
|
|
tmx_val = all_values[idx]
|
|
if tmx_val == 0:
|
|
yaml_val = -1
|
|
else:
|
|
yaml_val = tmx_val - offset
|
|
row.append(yaml_val)
|
|
result.append(row)
|
|
|
|
return result
|
|
|
|
|
|
def parse_tmx(tmx_path: Path) -> tuple[list[list[int]], list[list[int]] | None, int, int]:
|
|
"""
|
|
Parsea un archivo TMX y extrae el tilemap y collisionmap.
|
|
|
|
Returns:
|
|
tilemap: Lista de filas para el tilemap (layer id=1)
|
|
collisionmap: Lista de filas para el collisionmap (layer id=2), o None si no existe
|
|
width: Ancho del mapa en tiles
|
|
height: Alto del mapa en tiles
|
|
"""
|
|
tree = ET.parse(tmx_path)
|
|
root = tree.getroot()
|
|
|
|
width = int(root.attrib['width'])
|
|
height = int(root.attrib['height'])
|
|
|
|
# Obtener firstgid del primer tileset (para tilemap)
|
|
tileset = root.find('tileset')
|
|
firstgid = int(tileset.attrib['firstgid']) if tileset is not None else 1
|
|
|
|
# Buscar las capas por id
|
|
layers = root.findall('layer')
|
|
layer1 = None
|
|
layer2 = None
|
|
for layer in layers:
|
|
layer_id = layer.attrib.get('id')
|
|
if layer_id == '1':
|
|
layer1 = layer
|
|
elif layer_id == '2':
|
|
layer2 = layer
|
|
|
|
# Parsear layer 1 (tilemap) - obligatoria
|
|
if layer1 is None:
|
|
raise ValueError(f"No se encontro layer id=1 en {tmx_path}")
|
|
|
|
data1 = layer1.find('data')
|
|
if data1 is None:
|
|
raise ValueError(f"No se encontraron datos en layer 1 de {tmx_path}")
|
|
|
|
tilemap = parse_layer_csv(data1, width, height, firstgid)
|
|
|
|
# Parsear layer 2 (collisionmap) - opcional
|
|
collisionmap = None
|
|
if layer2 is not None:
|
|
data2 = layer2.find('data')
|
|
if data2 is not None:
|
|
# Para collisionmap: 0 -> -1, N -> N - 576
|
|
collisionmap = parse_layer_csv(data2, width, height, 576)
|
|
|
|
return tilemap, collisionmap, width, height
|
|
|
|
|
|
def format_tilemap_yaml(tilemap: list[list[int]], width: int, height: int) -> str:
|
|
"""
|
|
Formatea el tilemap como YAML.
|
|
"""
|
|
lines = []
|
|
lines.append(f"# Tilemap: {height} filas x {width} columnas ({width*8}x{height*8} pixeles @ 8px/tile)")
|
|
lines.append("# Indices de tiles (-1 = vacio)")
|
|
lines.append("tilemap:")
|
|
|
|
for row in tilemap:
|
|
row_str = ", ".join(str(v) for v in row)
|
|
lines.append(f" - [{row_str}]")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def format_collisionmap_yaml(collisionmap: list[list[int]], width: int, height: int) -> str:
|
|
"""
|
|
Formatea el collisionmap como YAML.
|
|
"""
|
|
lines = []
|
|
lines.append(f"# Collisionmap: {height} filas x {width} columnas")
|
|
lines.append("# Indices de colision (-1 = vacio, 1 = solido, 2 = plataforma)")
|
|
lines.append("collisionmap:")
|
|
|
|
for row in collisionmap:
|
|
row_str = ", ".join(str(v) for v in row)
|
|
lines.append(f" - [{row_str}]")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def update_yaml_tilemap(yaml_path: Path, tilemap: list[list[int]], collisionmap: list[list[int]] | None, width: int, height: int) -> bool:
|
|
"""
|
|
Actualiza la seccion tilemap y collisionmap de un archivo YAML.
|
|
|
|
Returns:
|
|
True si se actualizo correctamente, False si hubo error
|
|
"""
|
|
if not yaml_path.exists():
|
|
print(f" ADVERTENCIA: {yaml_path} no existe, saltando...")
|
|
return False
|
|
|
|
content = yaml_path.read_text(encoding='utf-8')
|
|
|
|
# Buscar donde empieza la seccion tilemap (incluyendo comentarios previos)
|
|
# Capturar cualquier bloque de comentarios seguidos justo antes de tilemap:
|
|
tilemap_pattern = re.compile(
|
|
r'(#[^\n]*\n)*' # Cualquier comentario antes de tilemap
|
|
r'tilemap:\s*\n' # Inicio de tilemap
|
|
r'( - \[.*\]\n?)+' # Filas del tilemap
|
|
)
|
|
|
|
match = tilemap_pattern.search(content)
|
|
if not match:
|
|
print(f" ERROR: No se encontro seccion tilemap en {yaml_path}")
|
|
return False
|
|
|
|
# Verificar que capturamos comentarios relacionados con tilemap
|
|
# Si el match incluye comentarios no relacionados, ajustar el inicio
|
|
matched_text = match.group(0)
|
|
lines = matched_text.split('\n')
|
|
|
|
# Buscar donde empiezan los comentarios de tilemap (los que queremos reemplazar)
|
|
start_idx = 0
|
|
for i, line in enumerate(lines):
|
|
if line.startswith('#') and ('tilemap' in line.lower() or 'indice' in line.lower() or 'tile' in line.lower()):
|
|
start_idx = i
|
|
break
|
|
elif line.startswith('tilemap:'):
|
|
start_idx = i
|
|
break
|
|
|
|
# Ajustar el inicio del match si hay comentarios que no queremos eliminar
|
|
if start_idx > 0:
|
|
skip_chars = sum(len(lines[i]) + 1 for i in range(start_idx))
|
|
actual_start = match.start() + skip_chars
|
|
else:
|
|
actual_start = match.start()
|
|
|
|
# Generar nuevo tilemap
|
|
new_tilemap = format_tilemap_yaml(tilemap, width, height)
|
|
|
|
# Reemplazar desde actual_start (que puede ser ajustado si hay comentarios no relacionados)
|
|
new_content = content[:actual_start] + new_tilemap + "\n" + content[match.end():]
|
|
|
|
# Procesar collisionmap si existe
|
|
if collisionmap is not None:
|
|
new_collisionmap = format_collisionmap_yaml(collisionmap, width, height)
|
|
|
|
# Buscar si ya existe seccion collisionmap
|
|
collisionmap_pattern = re.compile(
|
|
r'(#[^\n]*\n)*' # Cualquier comentario antes de collisionmap
|
|
r'collisionmap:\s*\n' # Inicio de collisionmap
|
|
r'( - \[.*\]\n?)+' # Filas del collisionmap
|
|
)
|
|
|
|
collision_match = collisionmap_pattern.search(new_content)
|
|
if collision_match:
|
|
# Reemplazar collisionmap existente
|
|
collision_matched_text = collision_match.group(0)
|
|
collision_lines = collision_matched_text.split('\n')
|
|
|
|
# Buscar donde empiezan los comentarios de collisionmap
|
|
collision_start_idx = 0
|
|
for i, line in enumerate(collision_lines):
|
|
if line.startswith('#') and ('collision' in line.lower()):
|
|
collision_start_idx = i
|
|
break
|
|
elif line.startswith('collisionmap:'):
|
|
collision_start_idx = i
|
|
break
|
|
|
|
if collision_start_idx > 0:
|
|
skip_chars = sum(len(collision_lines[i]) + 1 for i in range(collision_start_idx))
|
|
collision_actual_start = collision_match.start() + skip_chars
|
|
else:
|
|
collision_actual_start = collision_match.start()
|
|
|
|
new_content = new_content[:collision_actual_start] + new_collisionmap + "\n" + new_content[collision_match.end():]
|
|
else:
|
|
# Insertar collisionmap despues del tilemap
|
|
# Buscar el fin del tilemap recien insertado
|
|
tilemap_end_pattern = re.compile(r'tilemap:\s*\n( - \[.*\]\n?)+')
|
|
tilemap_end_match = tilemap_end_pattern.search(new_content)
|
|
if tilemap_end_match:
|
|
insert_pos = tilemap_end_match.end()
|
|
new_content = new_content[:insert_pos] + "\n" + new_collisionmap + "\n" + new_content[insert_pos:]
|
|
|
|
yaml_path.write_text(new_content, encoding='utf-8')
|
|
return True
|
|
|
|
|
|
def main():
|
|
# Directorio de habitaciones
|
|
script_dir = Path(__file__).parent
|
|
room_dir = script_dir.parent / "data" / "room"
|
|
|
|
if not room_dir.exists():
|
|
print(f"ERROR: No se encontro directorio {room_dir}")
|
|
return 1
|
|
|
|
# Buscar archivos TMX
|
|
tmx_files = sorted(room_dir.glob("*.tmx"))
|
|
|
|
if not tmx_files:
|
|
print("No se encontraron archivos .tmx en data/room/")
|
|
return 0
|
|
|
|
print(f"Encontrados {len(tmx_files)} archivos TMX")
|
|
print()
|
|
|
|
success_count = 0
|
|
error_count = 0
|
|
|
|
for tmx_path in tmx_files:
|
|
yaml_path = tmx_path.with_suffix('.yaml')
|
|
print(f"Procesando: {tmx_path.name} -> {yaml_path.name}")
|
|
|
|
try:
|
|
tilemap, collisionmap, width, height = parse_tmx(tmx_path)
|
|
print(f" TMX: {width}x{height} tiles")
|
|
if collisionmap is not None:
|
|
print(f" Collisionmap detectado")
|
|
|
|
if update_yaml_tilemap(yaml_path, tilemap, collisionmap, width, height):
|
|
print(f" OK: Tilemap actualizado")
|
|
if collisionmap is not None:
|
|
print(f" OK: Collisionmap actualizado")
|
|
success_count += 1
|
|
else:
|
|
error_count += 1
|
|
|
|
except Exception as e:
|
|
print(f" ERROR: {e}")
|
|
error_count += 1
|
|
|
|
print()
|
|
print(f"Completado: {success_count} exitosos, {error_count} errores")
|
|
return 0 if error_count == 0 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
exit(main())
|