feat(tools): 白模关卡自动生成Python脚本——Level Builder

两阶段流水线:搭建指南Markdown → JSON → UE5 Actor
- 解析器:4种表格变体+范围展开+坐标表达式计算
- 配置:37种BSP资产映射+13种材质+7区域定义
- 验证:265个Actor全部通过,0错误
- Builder:UE5 Python API自动放置Actor+材质+Outliner
- 报告:自动生成搭建报告Markdown

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 23:03:45 +08:00
parent 8011cb6472
commit 0f96b79dba
74 changed files with 10468 additions and 109 deletions
@@ -0,0 +1,103 @@
"""离线资产验证模块。
检查解析数据中的资产引用是否有效:
1. asset_key 是否在 asset_mapping 中定义
2. 材质名是否在 material_mapping 中定义
3. Content/ 目录下 .uasset 文件是否存在(可选)
"""
import logging
from pathlib import Path
from dataclasses import dataclass
from config.asset_mapping import MESH_ASSETS
from config.material_mapping import MATERIAL_PATHS
logger = logging.getLogger("level_builder.validator")
@dataclass
class ValidationResult:
"""验证结果。"""
total_actors: int = 0
valid_actors: int = 0
warnings: list[str] = None
errors: list[str] = None
missing_assets: set[str] = None
missing_materials: set[str] = None
def __post_init__(self):
if self.warnings is None:
self.warnings = []
if self.errors is None:
self.errors = []
if self.missing_assets is None:
self.missing_assets = set()
if self.missing_materials is None:
self.missing_materials = set()
@property
def is_valid(self) -> bool:
return len(self.errors) == 0
def validate_actors(actors: list[dict], project_root: str = "") -> ValidationResult:
"""验证 Actor 列表中的资产引用。
Args:
actors: Actor 数据列表(来自解析器的 JSON 输出)。
project_root: 项目根目录路径(用于文件存在性检查)。
Returns:
ValidationResult 包含所有验证信息。
"""
result = ValidationResult(total_actors=len(actors))
for i, actor in enumerate(actors):
name = actor.get("actor_name", f"<unnamed #{i}>")
asset_key = actor.get("asset_key", "")
material = actor.get("material", "")
asset_type = actor.get("asset_type", "static_mesh")
# 跳过非 mesh 类型(光照、PlayerStart 等)
if asset_type != "static_mesh":
result.valid_actors += 1
continue
# 检查 asset_key
if not asset_key:
result.warnings.append(f"{name}: 无 asset_key(可能是光照/特殊 Actor)")
result.valid_actors += 1
continue
if asset_key not in MESH_ASSETS:
result.errors.append(f"{name}: 未知资产 '{asset_key}'")
result.missing_assets.add(asset_key)
continue
# 检查材质
if material and material not in MATERIAL_PATHS:
result.warnings.append(f"{name}: 未知材质 '{material}'")
result.missing_materials.add(material)
# 检查 .uasset 文件存在性(如果提供了项目根目录)
if project_root:
mesh_info = MESH_ASSETS[asset_key]
asset_path = mesh_info["path"]
# /Game/BlockingStarterPack/Meshes/.../SM_Xxx.SM_Xxx → Content/BlockingStarterPack/Meshes/.../SM_Xxx.uasset
fs_path = asset_path.replace("/Game/", "Content/").split(".")[0] + ".uasset"
full_path = Path(project_root) / fs_path
if not full_path.exists():
result.warnings.append(f"{name}: 资产文件不存在 '{fs_path}'")
result.valid_actors += 1
# 汇总日志
if result.missing_assets:
logger.error(f"缺失资产定义: {sorted(result.missing_assets)}")
if result.missing_materials:
logger.warning(f"缺失材质定义: {sorted(result.missing_materials)}")
logger.info(f"验证完成: {result.valid_actors}/{result.total_actors} 有效, "
f"{len(result.errors)} 错误, {len(result.warnings)} 警告")
return result