| import hashlib
|
| import re
|
| from concurrent.futures import ThreadPoolExecutor
|
| from itertools import chain
|
| from typing import Any, Dict, List, Optional, Tuple
|
|
|
| import requests
|
|
|
|
|
|
|
|
|
| class Color:
|
| __slots__ = ("id", "index", "club", "selectable", "hex_code")
|
|
|
| def __init__(
|
| self, id: int, index: int, club: int, selectable: bool, hex_code: str
|
| ) -> None:
|
| self.id = id
|
| self.index = index
|
| self.club = club
|
| self.selectable = selectable
|
| self.hex_code = hex_code
|
|
|
| def __repr__(self) -> str:
|
| return f"<Color id={self.id} #{self.hex_code}>"
|
|
|
| def __eq__(self, other) -> bool:
|
| return isinstance(other, Color) and self.id == other.id
|
|
|
| def __hash__(self) -> int:
|
| return hash(self.id)
|
|
|
|
|
| class Palette:
|
| __slots__ = ("id", "colors")
|
|
|
| def __init__(self, id: int, colors: List[Color]) -> None:
|
| self.id = id
|
| self.colors = colors
|
|
|
| def __repr__(self) -> str:
|
| return f"<Palette id={self.id}, colors={len(self.colors)}>"
|
|
|
| def get_color_by_id(self, color_id: int) -> Optional[Color]:
|
| for c in self.colors:
|
| if c.id == color_id:
|
| return c
|
| return None
|
|
|
| def get_selectable_colors(self) -> List[Color]:
|
| return [c for c in self.colors if c.selectable]
|
|
|
|
|
| class Part:
|
| __slots__ = ("id", "type", "colorable", "index", "colorindex")
|
|
|
| def __init__(
|
| self,
|
| id: int,
|
| type: str,
|
| colorable: bool = False,
|
| index: int = 0,
|
| colorindex: int = 0,
|
| ) -> None:
|
| self.id = id
|
| self.type = type
|
| self.colorable = colorable
|
| self.index = index
|
| self.colorindex = colorindex
|
|
|
| def __repr__(self) -> str:
|
| return f"<<<parteId:{self.id},tipo:{self.type},colorindex:{self.colorindex}>>>"
|
|
|
| def __eq__(self, other) -> bool:
|
| if not isinstance(other, Part):
|
| return False
|
| return (
|
| self.id == other.id
|
| and self.type == other.type
|
| and self.colorindex == other.colorindex
|
| )
|
|
|
| def __hash__(self) -> int:
|
| return hash((self.id, self.type, self.colorindex))
|
|
|
|
|
| class Lib:
|
| __slots__ = (
|
| "id",
|
| "parts",
|
| "gender",
|
| "club",
|
| "colorable",
|
| "selectable",
|
| "preselectable",
|
| "sellable",
|
| "paleta",
|
| "type",
|
| )
|
|
|
| def __init__(
|
| self,
|
| id: int,
|
| parts: list[Part],
|
| gender: str | None = None,
|
| club: int = 0,
|
| colorable: bool = False,
|
| selectable: bool = False,
|
| preselectable: bool = False,
|
| sellable: bool = False,
|
| paleta: int = -1,
|
| type: str = "Desconocido",
|
| ) -> None:
|
| self.id = id
|
| self.parts = parts
|
| self.gender = gender
|
| self.club = club
|
| self.colorable = colorable
|
| self.selectable = selectable
|
| self.preselectable = preselectable
|
| self.sellable = sellable
|
| self.paleta = paleta
|
| self.type = type
|
|
|
| def __repr__(self) -> str:
|
| return f"ID:{self.id},partes:{len(self.parts)},gender:{self.gender}, paleta:{self.paleta},Tipo:{self.type}"
|
|
|
| def __eq__(self, other) -> bool:
|
| return isinstance(other, Lib) and self.parts == other.parts
|
|
|
| def set_paleta(self, pid):
|
| self.paleta = pid
|
|
|
| def copy(self):
|
| return Lib(
|
| id=self.id,
|
| parts=self.parts.copy(),
|
| gender=self.gender,
|
| club=self.club,
|
| colorable=self.colorable,
|
| selectable=self.selectable,
|
| preselectable=self.preselectable,
|
| sellable=self.sellable,
|
| paleta=self.paleta,
|
| type=self.type,
|
| )
|
|
|
|
|
| class Full(Lib):
|
| __slots__ = ("lib_id",)
|
|
|
| def __init__(self, obj: Lib, lib_id: str) -> None:
|
| super().__init__(
|
| id=obj.id,
|
| parts=obj.parts,
|
| gender=obj.gender,
|
| club=obj.club,
|
| colorable=obj.colorable,
|
| selectable=obj.selectable,
|
| preselectable=obj.preselectable,
|
| sellable=obj.sellable,
|
| paleta=obj.paleta,
|
| type=obj.type,
|
| )
|
| self.lib_id = lib_id
|
|
|
| def __repr__(self) -> str:
|
| return f"ID:{self.id},partes:{len(self.parts)},Lib:{self.lib_id},Paleta:{self.paleta}"
|
|
|
|
|
|
|
|
|
|
|
| def link_parts(partes: list[dict]) -> list[Part]:
|
| return [
|
| Part(
|
| id=p["id"],
|
| type="hr" if p["type"] == "hrb" else p["type"],
|
| colorable=p.get("colorable", False),
|
| index=p.get("index", 0),
|
| colorindex=p.get("colorindex", 0),
|
| )
|
| for p in partes
|
| ]
|
|
|
|
|
| def link_colors(colors_data: list[dict]) -> list[Color]:
|
| return [
|
| Color(
|
| id=c["id"],
|
| index=c["index"],
|
| club=c["club"],
|
| selectable=c["selectable"],
|
| hex_code=c["hexCode"],
|
| )
|
| for c in colors_data
|
| ]
|
|
|
|
|
| def link_palettes(palettes_data: list[dict]) -> list[Palette]:
|
| return [Palette(p["id"], link_colors(p["colors"])) for p in palettes_data]
|
|
|
|
|
| def hook(info: dict) -> Any:
|
| if "parts" in info:
|
| parts = link_parts(info["parts"])
|
| return Lib(
|
| id=info["id"],
|
| parts=parts,
|
| gender=info.get("gender"),
|
| club=info.get("club", 0),
|
| colorable=info.get("colorable", False),
|
| selectable=info.get("selectable", False),
|
| preselectable=info.get("preselectable", False),
|
| sellable=info.get("sellable", False),
|
| )
|
| return info
|
|
|
|
|
| def fetch_json(url: str) -> Any:
|
| response = requests.get(url.strip())
|
| response.raise_for_status()
|
| return response.json(object_hook=hook)
|
|
|
|
|
|
|
|
|
|
|
| def load_game_data(
|
| config_url: str, extra_vars: Dict[str, str] = None
|
| ) -> tuple[list[Lib], list[Lib], list[Palette], dict]:
|
| """
|
| Carga datos desde renderer-config.json.
|
| - extra_vars sobrescribe cualquier clave del JSON.
|
| - Convierte %libname% → {libname}.
|
| - Devuelve (figuremap, figuredata_flat, palettes, config_procesado).
|
| """
|
| if extra_vars is None:
|
| extra_vars = {}
|
|
|
|
|
| resp = requests.get(config_url.strip())
|
| resp.raise_for_status()
|
| raw_config = resp.json()
|
|
|
|
|
| config_with_overrides = {}
|
| for key in raw_config:
|
| config_with_overrides[key] = extra_vars.get(key, raw_config[key])
|
| for key in extra_vars:
|
| if key not in config_with_overrides:
|
| config_with_overrides[key] = extra_vars[key]
|
|
|
|
|
| def convert_percent_to_format(template: str) -> str:
|
| return re.sub(r"%([^%]+)%", r"{\1}", template)
|
|
|
| def preprocess_config(obj):
|
| if isinstance(obj, str):
|
| return convert_percent_to_format(obj)
|
| elif isinstance(obj, list):
|
| return [preprocess_config(item) for item in obj]
|
| elif isinstance(obj, dict):
|
| return {k: preprocess_config(v) for k, v in obj.items()}
|
| else:
|
| return obj
|
|
|
| config = preprocess_config(config_with_overrides)
|
|
|
|
|
| context = {}
|
| all_keys = set(config.keys())
|
|
|
| for key in all_keys:
|
| value = config[key]
|
| if isinstance(value, str) and not re.search(r"\$\{", value):
|
| context[key] = value.strip()
|
|
|
| changed = True
|
| while changed:
|
| changed = False
|
| for key in all_keys:
|
| value = config[key]
|
| if isinstance(value, str) and "${" in value:
|
|
|
| def replace_var(match):
|
| var_name = match.group(1)
|
| return context.get(var_name, match.group(0))
|
|
|
| resolved = re.sub(r"\$\{([^}]+)\}", replace_var, value).strip()
|
| if "${" not in resolved and context.get(key) != resolved:
|
| context[key] = resolved
|
| changed = True
|
|
|
|
|
| def escape_html(text: str) -> str:
|
| return (
|
| text.replace("&", "&")
|
| .replace("<", "<")
|
| .replace(">", ">")
|
| .replace('"', """)
|
| .replace("'", "'")
|
| )
|
|
|
| def debug_config(original_config: dict, ctx: dict) -> str:
|
| def resolve_value(value):
|
| if isinstance(value, str):
|
|
|
| def repl(m):
|
| var = m.group(1)
|
| return ctx.get(var, f"${{{var}}}")
|
|
|
| return re.sub(r"\$\{([^}]+)\}", repl, value)
|
| elif isinstance(value, list):
|
| return [resolve_value(v) for v in value]
|
| elif isinstance(value, dict):
|
| return {k: resolve_value(v) for k, v in value.items()}
|
| else:
|
| return value
|
|
|
| resolved = resolve_value(original_config)
|
| import json
|
|
|
| return json.dumps(resolved, indent=2, ensure_ascii=False)
|
|
|
| all_missing_vars = set()
|
| for key in ["avatar.figuremap.url", "avatar.figuredata.url"]:
|
| tpl = config.get(key, "")
|
| if isinstance(tpl, str):
|
| vars_in_tpl = re.findall(r"\$\{([^}]+)\}", tpl)
|
| all_missing_vars.update(vars_in_tpl)
|
|
|
| root_missing = set()
|
| visited = set()
|
|
|
| def find_root_vars(var_name):
|
| if var_name in visited:
|
| return
|
| visited.add(var_name)
|
| if var_name in context:
|
| return
|
| if var_name not in config:
|
| root_missing.add(var_name)
|
| return
|
| value = config[var_name]
|
| if isinstance(value, str):
|
| deps = re.findall(r"\$\{([^}]+)\}", value)
|
| for dep in deps:
|
| find_root_vars(dep)
|
|
|
| for var in all_missing_vars:
|
| find_root_vars(var)
|
|
|
| if root_missing:
|
| debug_str = debug_config(config, context)
|
| safe_debug = escape_html(debug_str)
|
| raise ValueError(
|
| f"Variables raíz no resueltas: {sorted(root_missing)}.\n\n"
|
| f"<pre>{safe_debug}</pre>\n\n"
|
| f"Pásalas en la URL como ?{'&'.join(f'{v}=...' for v in sorted(root_missing))}"
|
| )
|
|
|
|
|
| figuremap_url = context.get("avatar.figuremap.url")
|
| figuredata_url = context.get("avatar.figuredata.url")
|
|
|
| if not figuremap_url or not figuredata_url:
|
| raise ValueError("No se pudieron resolver las URLs de FigureMap o FigureData")
|
|
|
|
|
| with ThreadPoolExecutor(max_workers=2) as executor:
|
| future_map = executor.submit(fetch_json, figuremap_url)
|
| future_data = executor.submit(fetch_json, figuredata_url)
|
| figuremap_dict = future_map.result()
|
| figuredata_dict = future_data.result()
|
|
|
|
|
| def setPaleta(set_) -> List[Lib]:
|
| v: List[Lib] = set_["sets"]
|
| for i in range(len(v)):
|
| v[i].set_paleta(set_["paletteId"])
|
| v[i].type = set_["type"]
|
| return v
|
|
|
| figuremap_raw: List[Lib] = figuremap_dict["libraries"]
|
| figuremap_by_id = {lib.id: lib for lib in figuremap_raw}
|
| figuredata_flat: List[Lib] = list(
|
| chain.from_iterable(setPaleta(set_) for set_ in figuredata_dict["setTypes"])
|
| )
|
| figuremap = []
|
| for lib in figuremap_raw:
|
|
|
| matching_data = None
|
| for data_lib in figuredata_flat:
|
| if data_lib.id == lib.id:
|
| matching_data = data_lib
|
| break
|
|
|
| if matching_data and matching_data.paleta != -1:
|
| lib.set_paleta(matching_data.paleta)
|
| if matching_data and matching_data.type != "Desconocido":
|
| lib.type = matching_data.type
|
| figuremap.append(lib)
|
| palettes: List[Palette] = link_palettes(figuredata_dict["palettes"])
|
|
|
| def resolve_config_variables(config_dict: dict, context: dict) -> dict:
|
| """Resuelve todas las ${var} en todo el config usando el contexto."""
|
|
|
| def resolve_value(value):
|
| if isinstance(value, str):
|
|
|
| def repl(match):
|
| var = match.group(1)
|
| return context.get(var, match.group(0))
|
|
|
| return re.sub(r"\$\{([^}]+)\}", repl, value)
|
| elif isinstance(value, list):
|
| return [resolve_value(v) for v in value]
|
| elif isinstance(value, dict):
|
| return {k: resolve_value(v) for k, v in value.items()}
|
| else:
|
| return value
|
|
|
| return resolve_value(config_dict)
|
|
|
| resolved_config = resolve_config_variables(config, context)
|
|
|
| return figuremap, figuredata_flat, palettes, resolved_config
|
|
|
|
|
|
|
|
|
| pruebas = {"hair": "hr", "trousers": "lg", "hat": "ha"}
|
|
|
|
|
| def return_correct(name):
|
| for started in pruebas.keys():
|
| if name.startswith(started):
|
| return pruebas[started]
|
| return "Desconocido"
|
|
|
|
|
| def get_all_part_types_from_data(
|
| figuremap, figuredata_flat, include_all=True
|
| ) -> tuple[list[str], dict]:
|
| _category_index = {}
|
| if include_all:
|
| _category_index["Todos"] = []
|
|
|
| def parts_base_key(lib):
|
| return tuple((p.id, p.type) for p in lib.parts)
|
|
|
|
|
| figuremap_keys = set()
|
| figuremap_dict = {}
|
| for item in figuremap:
|
| key = parts_base_key(item)
|
| figuremap_keys.add(key)
|
| figuremap_dict[key] = item
|
|
|
| figuredata_by_base_key = {}
|
| for lib in figuredata_flat:
|
| key = parts_base_key(lib)
|
| if key not in figuredata_by_base_key:
|
| figuredata_by_base_key[key] = lib
|
|
|
|
|
| for item in figuremap:
|
| key = parts_base_key(item)
|
| if key in figuredata_by_base_key:
|
| matched_lib = figuredata_by_base_key[key]
|
|
|
| full = Full(matched_lib.copy(), str(item.id))
|
|
|
| full.set_paleta(matched_lib.paleta)
|
| full.type = matched_lib.type
|
| if full.type == "Desconocido":
|
| if len(set([t.type for t in full.parts])) == 1:
|
| full.type = full.parts[0].type
|
| else:
|
| full.type = return_correct(full.lib_id)
|
| if full.type == "Desconocido":
|
| full.type = full.parts[0].type
|
| print(full.lib_id, "->", full.type)
|
|
|
|
|
| if full.type not in _category_index:
|
| _category_index[full.type] = []
|
|
|
| _category_index[full.type].append(full)
|
| if include_all:
|
| _category_index["Todos"].append(full)
|
|
|
| extra_items = []
|
| for lib in figuredata_flat:
|
| key = parts_base_key(lib)
|
| if key not in figuremap_keys:
|
| full = Full(lib.copy(), str(lib.id))
|
| extra_items.append(full)
|
|
|
| if extra_items:
|
| _category_index["extra"] = extra_items
|
|
|
|
|
| missing_items = []
|
| for item in figuremap:
|
| key = parts_base_key(item)
|
| if key not in figuredata_by_base_key:
|
|
|
| dummy_lib = Lib(
|
| 0,
|
| item.parts,
|
| )
|
| full = Full(dummy_lib, str(item.id))
|
| missing_items.append(full)
|
|
|
| if missing_items:
|
| _category_index["missing"] = missing_items
|
|
|
| return sorted(_category_index.keys()), _category_index
|
|
|