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