Armario / cloth_loader.py
YoBatM's picture
Upload 4 files
c1d9028 verified
import hashlib
import re
from concurrent.futures import ThreadPoolExecutor
from itertools import chain
from typing import Any, Dict, List, Optional, Tuple
import requests
# =============== CLASES ===============
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}"
# =============== FUNCIONES AUXILIARES ===============
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)
# =============== CARGA DINÁMICA ===============
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 = {}
# 1. Descargar config
resp = requests.get(config_url.strip())
resp.raise_for_status()
raw_config = resp.json()
# 2. 🔥 APLICAR SOBREESCRITURA TOTAL
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]
# 3. Preprocesar: convertir %libname% → {libname}
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)
# 4. Resolver variables recursivamente
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
# 5. Detectar variables raíz faltantes
def escape_html(text: str) -> str:
return (
text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace('"', "&quot;")
.replace("'", "&#x27;")
)
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))}"
)
# 6. Obtener URLs reales
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")
# 7. Descargar datos
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()
# 8. Procesar
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:
# Buscar en figuredata_flat el mismo ID
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
# =============== FUNCIONES PÚBLICAS ===============
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)
# Indexar ambos conjuntos
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
# 1. Ítems normales (en ambos)
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.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)
# Encontrar la parte con mayor prioridad
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)
# 2. Ítems EXTRA (solo en figuredata)
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)) # ID 0 para ítems extra
extra_items.append(full)
if extra_items:
_category_index["extra"] = extra_items
# 3. Ítems MISSING (solo en figuremap)
missing_items = []
for item in figuremap:
key = parts_base_key(item)
if key not in figuredata_by_base_key:
# Crear un objeto Full "vacío" con datos mínimos
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