feat(gamedev): comfyui_build_structure_workflow — edificios/estructuras de escenario (building completo, view iso/lateral, alpha)
Builder gamedev-2d nuevo: edificacion grande y completa (casa, torre, castillo, tienda, posada, ruina, muralla, puente, templo, faro) para poblar mapas/escenarios. Diferenciado de comfyui_build_prop_object (edificio completo vs objeto pequeno suelto): el negativo rechaza small object/single item/prop/furniture y el scaffold empuja full building/ complete structure/single building. view (iso por defecto) fija la perspectiva del mapa. Pura (dict API format): compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo/iso opcional) + Image Rembg (alpha si transparent). 12 tests offline verdes. Probado e2e en GPU (8GB lowvram): medieval blacksmith shop iso 512x512 RGBA, edificio centrado (centroide 0.54/0.53). Fila en docs/capabilities/gamedev-2d.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,7 @@ VFX (ver `reports/0143`).
|
|||||||
| `comfyui_build_splash_art_workflow_py_ml` | `(scene, *, mood="epic, cinematic", checkpoint="juggernaut_xl_v11…", width=1024, height=576, hires=True, seed=0, lora=None, …) -> dict` | LA ilustración grande de UN splash / pantalla de carga / key art en formato **pantalla apaisado 16:9** (`width>height`, ~1024×576), composición cinematográfica (`{scene}, {mood}, key art, game splash screen, dramatic lighting, cinematic composition, wide shot, epic scale, atmospheric…`). `hires=True` → 2ª pasada de detalle (`comfyui_build_hires_fix_workflow`) para verse a pantalla completa; si no, txt2img + LoRA estilo opcional. Genera SOLO la ilustración — el título/logo/barra de carga los pone el motor/post (negativo rechaza `text/title/logo/UI/frame/watermark`), dejando aire para superponer el título. Set coherente = mismo `mood`/`checkpoint`/`lora`, varía solo `scene`. Probado e2e en GPU con SD1.5 + hires (1024×576 → 1536×864, 54s, `reports/0159`). SD1.5/SDXL. |
|
| `comfyui_build_splash_art_workflow_py_ml` | `(scene, *, mood="epic, cinematic", checkpoint="juggernaut_xl_v11…", width=1024, height=576, hires=True, seed=0, lora=None, …) -> dict` | LA ilustración grande de UN splash / pantalla de carga / key art en formato **pantalla apaisado 16:9** (`width>height`, ~1024×576), composición cinematográfica (`{scene}, {mood}, key art, game splash screen, dramatic lighting, cinematic composition, wide shot, epic scale, atmospheric…`). `hires=True` → 2ª pasada de detalle (`comfyui_build_hires_fix_workflow`) para verse a pantalla completa; si no, txt2img + LoRA estilo opcional. Genera SOLO la ilustración — el título/logo/barra de carga los pone el motor/post (negativo rechaza `text/title/logo/UI/frame/watermark`), dejando aire para superponer el título. Set coherente = mismo `mood`/`checkpoint`/`lora`, varía solo `scene`. Probado e2e en GPU con SD1.5 + hires (1024×576 → 1536×864, 54s, `reports/0159`). SD1.5/SDXL. |
|
||||||
| `comfyui_build_decal_overlay_workflow_py_ml` | `(decal, *, on_black=True, style="grunge decal, high detail", checkpoint="dreamshaper_8…", size=512, seed=0, lora=None, …) -> dict` | UN decal/overlay con alpha para superponer sobre superficies/paredes/sprites con blend mode del motor (sangre, grietas, suciedad, óxido, quemaduras, salpicaduras, arañazos, musgo): textura **aislada sobre fondo PLANO** (`{decal}, {style}, single isolated decal, centered, on a solid pure black background, flat backdrop, sticker, no scenery, texture overlay, game asset…`) → txt2img cuadrado + LoRA estilo opcional. `on_black=True` (defecto) pensado para extraer alpha con **`comfyui_matting_luma_to_alpha`** (luma=alpha, conserva el falloff de translúcidos — la técnica gamedev correcta, ≠ recorte binario). **NO inyecta Rembg** (el matting es luma→alpha de disco, no un nodo): el SaveImage sale directo del VAEDecode. Set coherente = mismo `style`/`checkpoint`/`lora`, varía solo `decal`/`seed`. ⚠️ "grunge" en `style` arrastra fondo gris en SD1.5 → para fondo negro plano usar un `style` sin connotación de fondo + reroll de `seed`; luma Rec601 penaliza el rojo → para sangre roja pasar `luma_weights` con más peso al rojo. Probado e2e en GPU con SD1.5 (`reports/0160`). SD1.5. |
|
| `comfyui_build_decal_overlay_workflow_py_ml` | `(decal, *, on_black=True, style="grunge decal, high detail", checkpoint="dreamshaper_8…", size=512, seed=0, lora=None, …) -> dict` | UN decal/overlay con alpha para superponer sobre superficies/paredes/sprites con blend mode del motor (sangre, grietas, suciedad, óxido, quemaduras, salpicaduras, arañazos, musgo): textura **aislada sobre fondo PLANO** (`{decal}, {style}, single isolated decal, centered, on a solid pure black background, flat backdrop, sticker, no scenery, texture overlay, game asset…`) → txt2img cuadrado + LoRA estilo opcional. `on_black=True` (defecto) pensado para extraer alpha con **`comfyui_matting_luma_to_alpha`** (luma=alpha, conserva el falloff de translúcidos — la técnica gamedev correcta, ≠ recorte binario). **NO inyecta Rembg** (el matting es luma→alpha de disco, no un nodo): el SaveImage sale directo del VAEDecode. Set coherente = mismo `style`/`checkpoint`/`lora`, varía solo `decal`/`seed`. ⚠️ "grunge" en `style` arrastra fondo gris en SD1.5 → para fondo negro plano usar un `style` sin connotación de fondo + reroll de `seed`; luma Rec601 penaliza el rojo → para sangre roja pasar `luma_weights` con más peso al rojo. Probado e2e en GPU con SD1.5 (`reports/0160`). SD1.5. |
|
||||||
| `comfyui_build_projectile_workflow_py_ml` | `(projectile, *, direction="right", glow=False, style="game projectile, side view", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN proyectil orientado (flecha, bala, bola de fuego, rayo, misil, hechizo): sprite pequeño con **orientación** (apunta a la derecha por defecto, ángulo 0 — el motor rota el sprite), aislado, listo para instanciar. **`glow` elige el camino a alpha**: `glow=False` (defecto) = proyectil SÓLIDO con silueta → `plain background` + **Rembg** (alpha por recorte, como `item_icon`/`topdown_sprite`); `glow=True` = brillante/mágico → `glowing, on black background` **sin Rembg** (recortaría el halo), insumo de **`comfyui_matting_luma_to_alpha`** que el caller aplica luego (como `vfx_spritesheet`/`decal_overlay`). `glow=True` ignora `transparent`/`rembg_model`; el negativo por defecto NO rechaza "black background". `direction` se inserta como `pointing {direction}` (`""`/None = sin orientación). Set coherente = mismo `style`/`checkpoint`/`lora`, varía solo `projectile`/`seed`. Probado e2e en GPU con SD1.5 — fireball glow sobre negro + luma→alpha RGBA (`reports/0161`). SD1.5. |
|
| `comfyui_build_projectile_workflow_py_ml` | `(projectile, *, direction="right", glow=False, style="game projectile, side view", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN proyectil orientado (flecha, bala, bola de fuego, rayo, misil, hechizo): sprite pequeño con **orientación** (apunta a la derecha por defecto, ángulo 0 — el motor rota el sprite), aislado, listo para instanciar. **`glow` elige el camino a alpha**: `glow=False` (defecto) = proyectil SÓLIDO con silueta → `plain background` + **Rembg** (alpha por recorte, como `item_icon`/`topdown_sprite`); `glow=True` = brillante/mágico → `glowing, on black background` **sin Rembg** (recortaría el halo), insumo de **`comfyui_matting_luma_to_alpha`** que el caller aplica luego (como `vfx_spritesheet`/`decal_overlay`). `glow=True` ignora `transparent`/`rembg_model`; el negativo por defecto NO rechaza "black background". `direction` se inserta como `pointing {direction}` (`""`/None = sin orientación). Set coherente = mismo `style`/`checkpoint`/`lora`, varía solo `projectile`/`seed`. Probado e2e en GPU con SD1.5 — fireball glow sobre negro + luma→alpha RGBA (`reports/0161`). SD1.5. |
|
||||||
|
| `comfyui_build_structure_workflow_py_ml` | `(structure, *, view="isometric", style="game building", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN edificio/estructura de escenario (casa, torre, castillo, tienda, posada, ruina, muralla, puente, templo, faro): UN **building COMPLETO** y centrado a perspectiva de juego (`{view} view`, iso por defecto), fondo limpio recortable a alpha (`{structure}, {view} view, {style}, full building, complete structure, single building, centered, plain background, game asset, architecture…`) → txt2img cuadrado + LoRA estilo/iso opcional + Rembg (alpha). **EDIFICACIÓN grande que ocupa varios tiles y define el escenario**, no un objeto pequeño suelto (≠ `prop_object`, que es atrezzo que se deja sobre un tile); el negativo rechaza `small object / single item / prop / furniture`. `view` fija la perspectiva del mapa (iso/side/front/top-down/¾); LoRA iso fija mejor el ángulo 2:1. Set coherente = mismo `view`/`style`/`checkpoint`/`lora`, varía solo `structure`. Probado e2e en GPU con SD1.5 — `medieval blacksmith shop` iso 512×512 RGBA, edificio centrado recortado a alpha (centroide 0.54/0.53, `reports/0164`). SD1.5. |
|
||||||
| `comfyui_build_particle_texture_workflow_py_ml` | `(particle, *, soft=True, style="particle texture, soft glow", checkpoint="dreamshaper_8…", size=256, seed=0, lora=None, …) -> dict` | UNA textura de **partícula individual** reutilizable (chispa, humo, polvo, destello/flare, gota, copo, hoja, círculo de energía) — el "ladrillo" que el sistema de partículas del motor (Godot `GPUParticles2D`, Unity VFX Graph) instancia a **miles** y anima (spawn/fade/color over lifetime). Aislada y centrada **sobre fondo NEGRO** (`{particle} particle, {style}, isolated on pure black background, <soft|sharp> edges, single element, for game particle system…`) → txt2img cuadrado + LoRA estilo opcional. **`soft` controla el borde**: `soft=True` (defecto) → `soft glow, feathered edges` (humo/destello/gota); `soft=False` → `crisp sharp edges, high contrast` (chispa/copo/hoja). **NO inyecta Rembg** (rompería el falloff translúcido): insumo de **`comfyui_matting_luma_to_alpha`** (luma=alpha, additive blend en el motor). **`size` por defecto pequeño (256)** porque se replica a miles. **DISTINTO de `vfx_spritesheet`** (ese es la SECUENCIA animada de un efecto; esto es UNA textura estática reutilizable) **y de `decal_overlay`** (ése es una mancha de desgaste estática para superponer; éste es un emisor de partículas). ⚠️ el `style` por defecto trae "soft glow" → si pides `soft=False` para algo nítido, usa un `style` sin connotación suave. Probado e2e en GPU con SD1.5 — `spark` 256×256 sobre negro plano (dark 85%) + luma→alpha RGBA con falloff preservado (`reports/0163`). SD1.5. |
|
| `comfyui_build_particle_texture_workflow_py_ml` | `(particle, *, soft=True, style="particle texture, soft glow", checkpoint="dreamshaper_8…", size=256, seed=0, lora=None, …) -> dict` | UNA textura de **partícula individual** reutilizable (chispa, humo, polvo, destello/flare, gota, copo, hoja, círculo de energía) — el "ladrillo" que el sistema de partículas del motor (Godot `GPUParticles2D`, Unity VFX Graph) instancia a **miles** y anima (spawn/fade/color over lifetime). Aislada y centrada **sobre fondo NEGRO** (`{particle} particle, {style}, isolated on pure black background, <soft|sharp> edges, single element, for game particle system…`) → txt2img cuadrado + LoRA estilo opcional. **`soft` controla el borde**: `soft=True` (defecto) → `soft glow, feathered edges` (humo/destello/gota); `soft=False` → `crisp sharp edges, high contrast` (chispa/copo/hoja). **NO inyecta Rembg** (rompería el falloff translúcido): insumo de **`comfyui_matting_luma_to_alpha`** (luma=alpha, additive blend en el motor). **`size` por defecto pequeño (256)** porque se replica a miles. **DISTINTO de `vfx_spritesheet`** (ese es la SECUENCIA animada de un efecto; esto es UNA textura estática reutilizable) **y de `decal_overlay`** (ése es una mancha de desgaste estática para superponer; éste es un emisor de partículas). ⚠️ el `style` por defecto trae "soft glow" → si pides `soft=False` para algo nítido, usa un `style` sin connotación suave. Probado e2e en GPU con SD1.5 — `spark` 256×256 sobre negro plano (dark 85%) + luma→alpha RGBA con falloff preservado (`reports/0163`). SD1.5. |
|
||||||
|
|
||||||
## Funciones de post-proceso y puente (`gamedev`, CPU)
|
## Funciones de post-proceso y puente (`gamedev`, CPU)
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
---
|
||||||
|
name: comfyui_build_structure_workflow
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: ml
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: pure
|
||||||
|
signature: "def comfyui_build_structure_workflow(structure: str, *, view: str = \"isometric\", style: str = \"game building\", checkpoint: str = \"dreamshaper_8.safetensors\", size: int = 512, transparent: bool = True, seed: int = 0, lora: str | None = None, lora_strength: float = 1.0, rembg_model: str = \"u2net\", negative: str | None = None, steps: int = 28, cfg: float = 7.0, sampler_name: str = \"dpmpp_2m\", scheduler: str = \"karras\", filename_prefix: str = \"structure\") -> dict"
|
||||||
|
description: "Construye el dict (API format) del workflow de UN edificio/estructura de escenario de juego 2D (casa, torre, castillo, tienda, posada, ruina, muralla, puente, templo, faro): UN building COMPLETO y centrado a perspectiva de juego (isometrica/lateral via view), fondo limpio uniforme recortable a alpha, estilo consistente para poblar mapas/escenarios. Diferenciado de comfyui_build_prop_object (edificacion grande completa vs objeto pequeno suelto). Compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo/iso opcional) + Image Rembg (fondo transparente si transparent). Hermano de comfyui_build_prop_object/isometric_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info."
|
||||||
|
tags: [comfyui, ml, gamedev, gamedev-2d, structure, building, architecture, scenery, environment, rembg, workflow]
|
||||||
|
uses_functions: [comfyui_build_txt2img_workflow_py_ml, comfyui_inject_lora_py_ml]
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: ""
|
||||||
|
imports: []
|
||||||
|
params:
|
||||||
|
- name: structure
|
||||||
|
desc: "Descripcion de la edificacion (ej. 'medieval house', 'wizard tower', 'stone castle', 'merchant shop', 'tavern inn', 'ruined temple', 'city wall', 'wooden bridge', 'lighthouse'). Se inserta en un prompt scaffold de building. No puede estar vacio."
|
||||||
|
- name: view
|
||||||
|
desc: "Perspectiva del juego con la que se muestra el edificio. Se inserta como '{view} view' en el prompt (ej. 'isometric', 'side', 'front', '3/4', 'top-down'). Por defecto 'isometric' (la mas comun para mapas iso). Vacio/None -> sin clausula de vista. keyword-only."
|
||||||
|
- name: style
|
||||||
|
desc: "Descriptor de estilo que mantiene consistentes las estructuras del set (ej. 'game building', 'low poly stylized building', 'pixel art building', 'fantasy RPG building', 'cartoon village building'). Pasa el MISMO view + style + checkpoint + lora a todos los edificios del mapa para coherencia. keyword-only."
|
||||||
|
- name: checkpoint
|
||||||
|
desc: "Checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5, holgado en 8GB lowvram) por defecto; 'juggernaut_xl_v11.safetensors' para SDXL (mas VRAM, subir size). keyword-only."
|
||||||
|
- name: size
|
||||||
|
desc: "Lado del cuadrado en px (width = height = size). 512 SD1.5 por defecto. keyword-only."
|
||||||
|
- name: transparent
|
||||||
|
desc: "Si True inyecta Image Rembg y el PNG sale con alpha (fondo recortado, listo para soltar sobre el mapa). False = edificio opaco sobre fondo plano, recortable luego por el caller. keyword-only."
|
||||||
|
- name: seed
|
||||||
|
desc: "Semilla del KSampler. Misma seed + misma structure/view/style -> mismo edificio. keyword-only."
|
||||||
|
- name: lora
|
||||||
|
desc: "LoRA de estilo/isometrica opcional en models/loras (ej. 'isometric_game_assets_sd15.safetensors', 'stylized_buildings_xl.safetensors'). Para escenarios iso coherentes la LoRA iso fija el angulo 2:1. None = sin LoRA. keyword-only."
|
||||||
|
- name: lora_strength
|
||||||
|
desc: "Fuerza del LoRA sobre model y clip. Se clampa a [0.0, 2.0]. keyword-only."
|
||||||
|
- name: rembg_model
|
||||||
|
desc: "Modelo Rembg ('u2net' general, 'isnet-anime' para anime). Solo se usa si transparent=True. keyword-only."
|
||||||
|
- name: negative
|
||||||
|
desc: "Prompt negativo. None usa el negativo por defecto pensado para estructuras (un edificio completo y entero, sin objetos pequenos/props, sin personas, fondo limpio, sin texto/recorte). keyword-only."
|
||||||
|
- name: steps
|
||||||
|
desc: "Pasos del KSampler. keyword-only."
|
||||||
|
- name: cfg
|
||||||
|
desc: "CFG del KSampler. keyword-only."
|
||||||
|
- name: sampler_name
|
||||||
|
desc: "Sampler del KSampler. keyword-only."
|
||||||
|
- name: scheduler
|
||||||
|
desc: "Scheduler del KSampler. keyword-only."
|
||||||
|
- name: filename_prefix
|
||||||
|
desc: "Prefijo del PNG en output/. keyword-only."
|
||||||
|
output: "dict en API format listo para comfyui_submit_workflow: base txt2img cuadrada con prompt scaffold de building ('{structure}, {view} view, {style}, full building, complete structure, single building, centered, plain background, game asset, architecture...') + LoRA de estilo opcional + Image Rembg (si transparent). UN edificio; para poblar un mapa -> llamar por cada structure con mismo view/style/checkpoint/lora; contact-sheet de las edificaciones -> montar los PNG con comfyui_build_grid."
|
||||||
|
tested: true
|
||||||
|
tests: ["golden transparent: clases CheckpointLoaderSimple/KSampler/VAEDecode/SaveImage/Image Rembg; structure + 'full building' + 'complete structure' + 'centered' + 'game asset' + 'plain background' + 'isometric view' en prompt; SaveImage <- Rembg; transparency True", "edge transparent=False: sin Rembg, SaveImage <- VAEDecode", "edge size: width==height==768 (cuadrado)", "edge structure al inicio del scaffold", "edge view reflejado como '{view} view'", "edge view vacio: sin clausula 'view' colgando", "edge style del set en prompt", "edge edificio completo (full building/single building) y negativo anti objeto-pequeno/prop/persona (diferenciado de prop_object)", "edge lora: LoraLoader presente con strength", "edge transparent default True", "error structure vacio -> ValueError", "determinismo"]
|
||||||
|
test_file_path: "python/functions/ml/comfyui_build_structure_workflow_test.py"
|
||||||
|
file_path: "python/functions/ml/comfyui_build_structure_workflow.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||||
|
from ml.comfyui_build_structure_workflow import comfyui_build_structure_workflow
|
||||||
|
|
||||||
|
# Un edificio de escenario con fondo transparente (alpha), listo para soltar en el mapa.
|
||||||
|
wf = comfyui_build_structure_workflow(
|
||||||
|
"medieval blacksmith shop",
|
||||||
|
view="isometric",
|
||||||
|
style="game building",
|
||||||
|
transparent=True,
|
||||||
|
seed=7,
|
||||||
|
)
|
||||||
|
# Poblar un mapa: variar `structure` con el MISMO view/style/checkpoint/(lora) para coherencia.
|
||||||
|
# for s in ["medieval house", "wizard tower", "stone castle", "tavern inn", "lighthouse"]:
|
||||||
|
# wf = comfyui_build_structure_workflow(s, view="isometric", style="game building", seed=7)
|
||||||
|
# comfyui_submit_workflow(wf) # -> comfyui_wait_result -> comfyui_fetch_output_image
|
||||||
|
# Contact-sheet de las edificaciones: montar los PNG resultantes con comfyui_build_grid.
|
||||||
|
```
|
||||||
|
|
||||||
|
O lanzable directo con: `./fn run comfyui_build_structure_workflow` (imprime nodos + class_types del ejemplo).
|
||||||
|
|
||||||
|
## Cuando usarla
|
||||||
|
|
||||||
|
Cuando necesites edificios/estructuras completas para poblar mapas o escenarios de un
|
||||||
|
juego (RPG, estrategia, isométrico, top-down, plataformas): casas, torres, castillos,
|
||||||
|
tiendas, posadas, ruinas, murallas, puentes, templos, faros. A diferencia de
|
||||||
|
`comfyui_build_prop_object_workflow` (un objeto PEQUEÑO suelto: barril, cofre, antorcha
|
||||||
|
que se deja encima de un tile), aquí el asset es una EDIFICACIÓN grande y completa que
|
||||||
|
ocupa varios tiles y define el escenario (la casa donde entra el jugador, la torre del
|
||||||
|
mapa, el castillo del boss). Fija `view` (`isometric` por defecto) para la perspectiva
|
||||||
|
del mapa y pasa el MISMO `view` + `style` + `checkpoint` + (`lora`) a todos los edificios
|
||||||
|
del set para que combinen; varía solo `structure`. `transparent` recorta el fondo (alpha)
|
||||||
|
listo para soltar sobre el mapa. Para un atlas/contact-sheet de las edificaciones, genera
|
||||||
|
cada una y monta los PNG con `comfyui_build_grid`.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **Structure (edificio completo) != prop_object (objeto pequeño suelto)**: si lo que
|
||||||
|
quieres es un objeto de escena pequeño (barril, cofre, planta, roca) que se deja sobre
|
||||||
|
un tile, usa `comfyui_build_prop_object_workflow`. Este builder genera la EDIFICACIÓN
|
||||||
|
grande completa (`full building / single building`) que ocupa varios tiles; el negativo
|
||||||
|
por defecto rechaza `small object / single item / prop / furniture` para no degradar a
|
||||||
|
un objeto suelto.
|
||||||
|
- **El recorte usa Rembg, NO luma-to-alpha**: un edificio es una masa sólida con silueta
|
||||||
|
definida (muros, tejado, torres), rembg lo recorta limpio. `comfyui_matting_luma_to_alpha`
|
||||||
|
es para translúcidos sobre negro (humo/fuego/magia). Si la estructura tiene partes
|
||||||
|
etéreas/translúcidas que quieras conservar, pon `transparent=False` y recorta aparte.
|
||||||
|
- **`view` fija la perspectiva del mapa**: `isometric` (default) para mapas iso 2:1;
|
||||||
|
`side`/`front` para plataformas; `top-down` para cenital; `3/4` para vista de ¾. Para
|
||||||
|
iso estricto, añade la LoRA iso (`lora="isometric_game_assets_sd15.safetensors"`), que
|
||||||
|
fija mejor el ángulo 2:1 que solo el prompt.
|
||||||
|
- **Coherencia del set = mismos parámetros**: si cambias `view`/`style`/`checkpoint`/`lora`/
|
||||||
|
`seed` entre edificios, el escenario deja de combinar. Fija esos y varía solo `structure`.
|
||||||
|
- **SDXL pide más VRAM y resolución**: con `checkpoint="juggernaut_xl_v11.safetensors"`
|
||||||
|
sube `size` a 768/1024; con dreamshaper_8 (SD1.5) deja 512 (holgado en 8GB lowvram). Si
|
||||||
|
hay OOM, baja `size` o usa SD1.5.
|
||||||
|
- Si el modelo mete varios edificios, personas o lo encoge a un objeto pequeño, el
|
||||||
|
negativo por defecto ya empuja a "single building / no people / no small object";
|
||||||
|
refuerza `style` con "single isolated building, full structure" si insiste.
|
||||||
|
- `transparent=False` deja el edificio opaco sobre fondo plano: útil si prefieres recortar
|
||||||
|
fuera del workflow o el motor compone sobre un fondo sólido.
|
||||||
|
- Es una función **pura**: solo arma el dict. La generación real (GPU) la hacen
|
||||||
|
`comfyui_submit_workflow` + `comfyui_wait_result` + `comfyui_fetch_output_image`.
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
"""Construye el workflow ComfyUI de UN edificio/estructura de escenario de juego (API format).
|
||||||
|
|
||||||
|
Estructura/edificio de mundo (casa, torre, castillo, tienda, posada, ruina, muralla,
|
||||||
|
puente, templo, almacen, faro...): UN building COMPLETO y centrado, con la perspectiva
|
||||||
|
del juego (isometrica o lateral), fondo limpio y uniforme recortable a alpha, estilo
|
||||||
|
consistente para poblar mapas/escenarios. Es el builder hermano de
|
||||||
|
comfyui_build_prop_object_workflow / comfyui_build_isometric_workflow: mismo patron
|
||||||
|
(PURO, dict API format) que compone funciones existentes del registry, no reescribe el
|
||||||
|
grafo.
|
||||||
|
|
||||||
|
Diferencia con prop_object (clave para no duplicar): un *prop* es un objeto de escena
|
||||||
|
PEQUENO y suelto (barril, cofre, antorcha, planta, roca) que se deja encima de un tile.
|
||||||
|
Una *structure* es una EDIFICACION grande y completa que ocupa varios tiles y define el
|
||||||
|
escenario (la casa donde entra el jugador, la torre del mapa, el castillo del boss). Por
|
||||||
|
eso el scaffold empuja "full building, complete structure" y el negativo rechaza
|
||||||
|
"small object, single item, prop" para forzar el edificio entero, no un objeto suelto.
|
||||||
|
|
||||||
|
Cableado:
|
||||||
|
|
||||||
|
CheckpointLoaderSimple -> [LoraLoader opcional de estilo/iso] -> KSampler
|
||||||
|
-> CLIPTextEncode (prompt scaffold de structure) ...
|
||||||
|
-> VAEDecode -> [Image Rembg opcional] -> SaveImage
|
||||||
|
|
||||||
|
Compone:
|
||||||
|
- comfyui_build_txt2img_workflow -> base txt2img cuadrada
|
||||||
|
- comfyui_inject_lora -> LoRA de estilo/isometrica opcional (consistencia)
|
||||||
|
- 'Image Rembg (Remove Background)' (helper local) -> fondo transparente (alpha)
|
||||||
|
|
||||||
|
Por que Rembg y NO comfyui_matting_luma_to_alpha: un edificio es una masa SOLIDA con
|
||||||
|
silueta definida (muros, tejado, torres); rembg recorta limpio la silueta dejando alpha,
|
||||||
|
listo para soltar sobre el mapa. La luma-to-alpha es para translucidos sobre negro
|
||||||
|
(humo/fuego/magia), donde aplanaria la estructura. Para una edificacion solida rembg es
|
||||||
|
lo correcto. Si la estructura es etérea o tiene partes translucidas que quieras conservar,
|
||||||
|
pon transparent=False y recorta fuera del workflow.
|
||||||
|
|
||||||
|
Por que un solo edificio centrado y fondo plano: una structure se inserta como sprite/
|
||||||
|
objeto grande en el motor; el scaffold empuja a "single building, centered, plain
|
||||||
|
background, game asset" y el negativo por defecto rechaza "multiple buildings, small
|
||||||
|
object, prop, person, character, cropped, out of frame" para mantener UN edificio entero
|
||||||
|
y recortable.
|
||||||
|
|
||||||
|
class_types/inputs verificados contra /object_info del servidor (8GB lowvram):
|
||||||
|
CheckpointLoaderSimple, CLIPTextEncode, EmptyLatentImage, KSampler, VAEDecode, SaveImage,
|
||||||
|
LoraLoader, 'Image Rembg (Remove Background)' (transparency BOOLEAN).
|
||||||
|
|
||||||
|
Funcion pura: sin red, sin I/O. No muta dicts de entrada (copia profunda en el helper de
|
||||||
|
rembg). Determinista para los mismos argumentos.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
|
||||||
|
# Negativo por defecto pensado para edificios/estructuras: UN building completo y entero,
|
||||||
|
# fondo limpio, sin objetos pequenos/props sueltos (eso es prop_object), sin personas, sin
|
||||||
|
# texto/marcas ni recortes. No filtra ningun tipo de edificio (casa, torre, castillo,
|
||||||
|
# muralla, puente... son validos).
|
||||||
|
_STRUCTURE_NEGATIVE = (
|
||||||
|
"small object, single item, prop, furniture, icon, "
|
||||||
|
"multiple buildings, cityscape, person, people, character, creature, "
|
||||||
|
"cluttered, blurry, lowres, deformed, bad perspective, "
|
||||||
|
"text, watermark, signature, logo, photo, photorealistic, "
|
||||||
|
"cropped, cut off, out of frame, jpeg artifacts"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _inject_rembg(workflow: dict, model: str) -> dict:
|
||||||
|
"""Inserta 'Image Rembg (Remove Background)' (transparency=True) entre VAEDecode y SaveImage.
|
||||||
|
|
||||||
|
Mismo helper que usan comfyui_build_prop_object_workflow / item_icon: el nodo recorta
|
||||||
|
la silueta del edificio dejando alpha. Repunta SaveImage.images a la salida del Rembg.
|
||||||
|
"""
|
||||||
|
wf = copy.deepcopy(workflow)
|
||||||
|
vaedecode_id = next(
|
||||||
|
(nid for nid, n in wf.items() if n.get("class_type") == "VAEDecode"), None
|
||||||
|
)
|
||||||
|
save_id = next((nid for nid, n in wf.items() if n.get("class_type") == "SaveImage"), None)
|
||||||
|
if vaedecode_id is None or save_id is None:
|
||||||
|
raise ValueError(
|
||||||
|
"comfyui_build_structure_workflow: no se encontro VAEDecode/SaveImage para Rembg"
|
||||||
|
)
|
||||||
|
numeric = [int(k) for k in wf.keys() if str(k).isdigit()]
|
||||||
|
rembg_id = str((max(numeric) + 1) if numeric else len(wf) + 1)
|
||||||
|
wf[rembg_id] = {
|
||||||
|
"class_type": "Image Rembg (Remove Background)",
|
||||||
|
"inputs": {
|
||||||
|
"images": [vaedecode_id, 0],
|
||||||
|
"transparency": True,
|
||||||
|
"model": model,
|
||||||
|
"post_processing": False,
|
||||||
|
"only_mask": False,
|
||||||
|
"alpha_matting": False,
|
||||||
|
"alpha_matting_foreground_threshold": 240,
|
||||||
|
"alpha_matting_background_threshold": 10,
|
||||||
|
"alpha_matting_erode_size": 10,
|
||||||
|
"background_color": "none",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
wf[save_id]["inputs"]["images"] = [rembg_id, 0]
|
||||||
|
return wf
|
||||||
|
|
||||||
|
|
||||||
|
def comfyui_build_structure_workflow(
|
||||||
|
structure: str,
|
||||||
|
*,
|
||||||
|
view: str = "isometric",
|
||||||
|
style: str = "game building",
|
||||||
|
checkpoint: str = "dreamshaper_8.safetensors",
|
||||||
|
size: int = 512,
|
||||||
|
transparent: bool = True,
|
||||||
|
seed: int = 0,
|
||||||
|
lora: str | None = None,
|
||||||
|
lora_strength: float = 1.0,
|
||||||
|
rembg_model: str = "u2net",
|
||||||
|
negative: str | None = None,
|
||||||
|
steps: int = 28,
|
||||||
|
cfg: float = 7.0,
|
||||||
|
sampler_name: str = "dpmpp_2m",
|
||||||
|
scheduler: str = "karras",
|
||||||
|
filename_prefix: str = "structure",
|
||||||
|
) -> dict:
|
||||||
|
"""Construye el dict (API format) del workflow de un edificio/estructura de escenario.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
structure: descripcion de la edificacion (ej. "medieval house", "wizard tower",
|
||||||
|
"stone castle", "merchant shop", "tavern inn", "ruined temple", "city wall",
|
||||||
|
"wooden bridge", "lighthouse"). Se inserta en un prompt scaffold de building.
|
||||||
|
No puede estar vacio.
|
||||||
|
view: perspectiva del juego con la que se muestra el edificio. Se inserta como
|
||||||
|
"{view} view" en el prompt (ej. "isometric", "side", "front", "3/4",
|
||||||
|
"top-down"). Por defecto "isometric" (la mas comun para poblar mapas iso).
|
||||||
|
keyword-only.
|
||||||
|
style: descriptor de estilo que mantiene consistentes las estructuras del set
|
||||||
|
(ej. "game building", "low poly stylized building", "pixel art building",
|
||||||
|
"fantasy RPG building", "cartoon village building"). Pasa el MISMO view +
|
||||||
|
style + checkpoint + (lora) a todos los edificios del mapa para coherencia
|
||||||
|
visual. keyword-only.
|
||||||
|
checkpoint: checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5, holgado
|
||||||
|
en 8GB lowvram) por defecto; 'juggernaut_xl_v11.safetensors' para SDXL (mas
|
||||||
|
VRAM, subir size a 768/1024). keyword-only.
|
||||||
|
size: lado del cuadrado en px (width = height = size). 512 SD1.5 por defecto.
|
||||||
|
keyword-only.
|
||||||
|
transparent: si True inyecta Image Rembg y el PNG sale con alpha (fondo recortado,
|
||||||
|
listo para soltar sobre el mapa). Si False deja el edificio opaco sobre fondo
|
||||||
|
plano, recortable luego por el caller/pipeline. keyword-only.
|
||||||
|
seed: semilla del KSampler. Misma seed + misma structure/view/style -> mismo
|
||||||
|
edificio. keyword-only.
|
||||||
|
lora: LoRA de estilo/isometrica opcional en models/loras (ej.
|
||||||
|
'isometric_game_assets_sd15.safetensors', 'stylized_buildings_xl.safetensors').
|
||||||
|
Para escenarios isometricos coherentes, la LoRA iso ayuda a fijar el angulo
|
||||||
|
2:1. None = sin LoRA. keyword-only.
|
||||||
|
lora_strength: fuerza del LoRA sobre model y clip. Se clampa a [0.0, 2.0].
|
||||||
|
keyword-only.
|
||||||
|
rembg_model: modelo Rembg ('u2net' general, 'isnet-anime' para anime). Solo se usa
|
||||||
|
si transparent=True. keyword-only.
|
||||||
|
negative: prompt negativo. None usa el negativo por defecto pensado para
|
||||||
|
estructuras (un edificio completo y entero, sin objetos pequenos/props, sin
|
||||||
|
personas, fondo limpio, sin texto/recorte). keyword-only.
|
||||||
|
steps, cfg, sampler_name, scheduler, filename_prefix: parametros de generacion.
|
||||||
|
keyword-only.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict en API format listo para comfyui_submit_workflow: txt2img base cuadrada con
|
||||||
|
prompt scaffold de building ('{structure}, {view} view, {style}, full building,
|
||||||
|
complete structure, centered, plain background, game asset, ...') + LoRA de estilo
|
||||||
|
opcional + Image Rembg (si transparent). Es UN edificio; para poblar un mapa ->
|
||||||
|
llamar por cada structure con el mismo view/style/checkpoint/(lora). Montar el set
|
||||||
|
con comfyui_build_grid si se quiere un contact-sheet de las edificaciones.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: si structure esta vacio, o si la base no tiene VAEDecode/SaveImage
|
||||||
|
donde inyectar el Rembg (propagado por el helper).
|
||||||
|
"""
|
||||||
|
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
|
||||||
|
|
||||||
|
if not structure or not structure.strip():
|
||||||
|
raise ValueError(
|
||||||
|
"comfyui_build_structure_workflow: 'structure' no puede estar vacio"
|
||||||
|
)
|
||||||
|
|
||||||
|
structure = structure.strip()
|
||||||
|
view = (view or "").strip()
|
||||||
|
lora_strength = max(0.0, min(2.0, float(lora_strength)))
|
||||||
|
neg = _STRUCTURE_NEGATIVE if negative is None else negative
|
||||||
|
|
||||||
|
# Prompt scaffold de structure: un edificio COMPLETO (no un objeto suelto), centrado,
|
||||||
|
# fondo plano, listo como asset de juego grande recortable. El "{view} view" fija la
|
||||||
|
# perspectiva del escenario.
|
||||||
|
view_clause = f"{view} view, " if view else ""
|
||||||
|
positive = (
|
||||||
|
f"{structure}, {view_clause}{style}, full building, complete structure, "
|
||||||
|
"single building, centered, plain background, game asset, "
|
||||||
|
"architecture, exterior, high detail"
|
||||||
|
)
|
||||||
|
|
||||||
|
wf = comfyui_build_txt2img_workflow(
|
||||||
|
checkpoint,
|
||||||
|
positive,
|
||||||
|
neg,
|
||||||
|
steps=steps,
|
||||||
|
cfg=cfg,
|
||||||
|
width=size,
|
||||||
|
height=size,
|
||||||
|
seed=seed,
|
||||||
|
sampler_name=sampler_name,
|
||||||
|
scheduler=scheduler,
|
||||||
|
filename_prefix=filename_prefix,
|
||||||
|
)
|
||||||
|
|
||||||
|
if lora:
|
||||||
|
from ml.comfyui_inject_lora import comfyui_inject_lora
|
||||||
|
|
||||||
|
wf = comfyui_inject_lora(
|
||||||
|
wf, lora, strength_model=lora_strength, strength_clip=lora_strength
|
||||||
|
)
|
||||||
|
|
||||||
|
if transparent:
|
||||||
|
wf = _inject_rembg(wf, rembg_model)
|
||||||
|
|
||||||
|
return wf
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import json
|
||||||
|
|
||||||
|
wf = comfyui_build_structure_workflow(
|
||||||
|
"medieval blacksmith shop",
|
||||||
|
view="isometric",
|
||||||
|
style="game building",
|
||||||
|
transparent=True,
|
||||||
|
seed=7,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"nodes": list(wf),
|
||||||
|
"classes": sorted({n["class_type"] for n in wf.values()}),
|
||||||
|
},
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
"""Tests offline de comfyui_build_structure_workflow (estructura del dict, sin GPU)."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
from ml.comfyui_build_structure_workflow import ( # noqa: E402
|
||||||
|
comfyui_build_structure_workflow,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _classes(wf):
|
||||||
|
return sorted({n["class_type"] for n in wf.values()})
|
||||||
|
|
||||||
|
|
||||||
|
def _by_class(wf, cls):
|
||||||
|
return [n for n in wf.values() if n["class_type"] == cls]
|
||||||
|
|
||||||
|
|
||||||
|
def _id_of(wf, cls):
|
||||||
|
return next(nid for nid, n in wf.items() if n["class_type"] == cls)
|
||||||
|
|
||||||
|
|
||||||
|
def _pos_with(wf, needle):
|
||||||
|
return next(
|
||||||
|
n for n in wf.values()
|
||||||
|
if n["class_type"] == "CLIPTextEncode" and needle in n["inputs"]["text"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_golden_transparent_recipe():
|
||||||
|
wf = comfyui_build_structure_workflow(
|
||||||
|
"medieval blacksmith shop", view="isometric", transparent=True, seed=7
|
||||||
|
)
|
||||||
|
cls = _classes(wf)
|
||||||
|
# Cadena base txt2img + Rembg para alpha.
|
||||||
|
assert "CheckpointLoaderSimple" in cls
|
||||||
|
assert "KSampler" in cls
|
||||||
|
assert "VAEDecode" in cls
|
||||||
|
assert "SaveImage" in cls
|
||||||
|
assert "Image Rembg (Remove Background)" in cls
|
||||||
|
# La structure aparece en el prompt positivo con el scaffold de edificio completo.
|
||||||
|
pos = _pos_with(wf, "medieval blacksmith shop")
|
||||||
|
txt = pos["inputs"]["text"]
|
||||||
|
assert "full building" in txt
|
||||||
|
assert "complete structure" in txt
|
||||||
|
assert "centered" in txt
|
||||||
|
assert "game asset" in txt
|
||||||
|
assert "plain background" in txt
|
||||||
|
# view reflejado como "{view} view".
|
||||||
|
assert "isometric view" in txt
|
||||||
|
# SaveImage toma la imagen del Rembg (no del VAEDecode).
|
||||||
|
rembg_id = _id_of(wf, "Image Rembg (Remove Background)")
|
||||||
|
save = next(n for n in wf.values() if n["class_type"] == "SaveImage")
|
||||||
|
assert save["inputs"]["images"] == [rembg_id, 0]
|
||||||
|
assert _by_class(wf, "Image Rembg (Remove Background)")[0]["inputs"]["transparency"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_opaque_no_rembg():
|
||||||
|
wf = comfyui_build_structure_workflow("stone castle", transparent=False)
|
||||||
|
assert "Image Rembg (Remove Background)" not in _classes(wf)
|
||||||
|
# SaveImage toma del VAEDecode directamente.
|
||||||
|
vd_id = _id_of(wf, "VAEDecode")
|
||||||
|
save = next(n for n in wf.values() if n["class_type"] == "SaveImage")
|
||||||
|
assert save["inputs"]["images"] == [vd_id, 0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_size_reflected():
|
||||||
|
wf = comfyui_build_structure_workflow("wizard tower", size=768)
|
||||||
|
latent = _by_class(wf, "EmptyLatentImage")[0]["inputs"]
|
||||||
|
assert latent["width"] == 768
|
||||||
|
assert latent["height"] == 768 # cuadrado
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_structure_at_start():
|
||||||
|
# El scaffold arranca directamente con la structure.
|
||||||
|
wf = comfyui_build_structure_workflow("tavern inn", transparent=False)
|
||||||
|
pos = _pos_with(wf, "tavern inn")
|
||||||
|
assert pos["inputs"]["text"].startswith("tavern inn")
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_view_reflected():
|
||||||
|
# view distinto del default reflejado como "{view} view".
|
||||||
|
wf = comfyui_build_structure_workflow(
|
||||||
|
"wooden bridge", view="side", transparent=False
|
||||||
|
)
|
||||||
|
txt = _pos_with(wf, "wooden bridge")["inputs"]["text"]
|
||||||
|
assert "side view" in txt
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_view_empty_no_clause():
|
||||||
|
# view vacio -> no se inserta clausula "view" colgando.
|
||||||
|
wf = comfyui_build_structure_workflow("city wall", view="", transparent=False)
|
||||||
|
txt = _pos_with(wf, "city wall")["inputs"]["text"]
|
||||||
|
assert " view," not in txt
|
||||||
|
assert "full building" in txt
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_style_in_prompt():
|
||||||
|
# Estilo del set reflejado.
|
||||||
|
wf = comfyui_build_structure_workflow(
|
||||||
|
"ruined temple", style="pixel art building", transparent=False
|
||||||
|
)
|
||||||
|
pos = _pos_with(wf, "ruined temple")
|
||||||
|
assert "pixel art building" in pos["inputs"]["text"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_building_not_small_prop():
|
||||||
|
# Por defecto el scaffold lo trata como edificio COMPLETO, no objeto pequeno suelto.
|
||||||
|
wf = comfyui_build_structure_workflow("lighthouse", transparent=False)
|
||||||
|
txt = _pos_with(wf, "lighthouse")["inputs"]["text"]
|
||||||
|
assert "full building" in txt
|
||||||
|
assert "single building" in txt
|
||||||
|
# El negativo por defecto rechaza objeto pequeno/prop (diferenciado de prop_object).
|
||||||
|
neg = next(
|
||||||
|
n for n in wf.values()
|
||||||
|
if n["class_type"] == "CLIPTextEncode" and "small object" in n["inputs"]["text"]
|
||||||
|
)
|
||||||
|
assert "prop" in neg["inputs"]["text"]
|
||||||
|
assert "single item" in neg["inputs"]["text"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_lora_reflected():
|
||||||
|
wf = comfyui_build_structure_workflow(
|
||||||
|
"wizard tower",
|
||||||
|
lora="isometric_game_assets_sd15.safetensors",
|
||||||
|
lora_strength=0.9,
|
||||||
|
)
|
||||||
|
loras = _by_class(wf, "LoraLoader")
|
||||||
|
assert len(loras) == 1
|
||||||
|
assert loras[0]["inputs"]["lora_name"] == "isometric_game_assets_sd15.safetensors"
|
||||||
|
assert loras[0]["inputs"]["strength_model"] == 0.9
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_transparent_default_true():
|
||||||
|
# transparent por defecto True -> Rembg presente sin pasar el flag.
|
||||||
|
wf = comfyui_build_structure_workflow("stone castle")
|
||||||
|
assert "Image Rembg (Remove Background)" in _classes(wf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_empty_structure():
|
||||||
|
try:
|
||||||
|
comfyui_build_structure_workflow(" ")
|
||||||
|
assert False
|
||||||
|
except ValueError as e:
|
||||||
|
assert "structure" in str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def test_determinism():
|
||||||
|
a = comfyui_build_structure_workflow(
|
||||||
|
"wizard tower", lora="stylized_buildings_xl.safetensors", seed=7
|
||||||
|
)
|
||||||
|
b = comfyui_build_structure_workflow(
|
||||||
|
"wizard tower", lora="stylized_buildings_xl.safetensors", seed=7
|
||||||
|
)
|
||||||
|
assert a == b
|
||||||
Reference in New Issue
Block a user