Files
pollo/tools/tmx_to_yaml.py
2025-11-23 11:44:31 +01:00

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())