primer commit
This commit is contained in:
293
tools/tmx_to_yaml.py
Normal file
293
tools/tmx_to_yaml.py
Normal file
@@ -0,0 +1,293 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user