Files
starmen1265 0f96b79dba 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>
2026-06-07 23:03:45 +08:00

200 lines
6.9 KiB
Python

"""Level Builder CLI — 白模关卡自动生成工具。
用法:
# Stage 1: 解析搭建指南 → JSON
python main.py parse --input ../../docs/technical/关卡-v01-搭建指南.md --output output/level_v01.json
# Stage 1.5: 验证
python main.py validate --input output/level_v01.json [--project-root ../..]
# Stage 2: UE5 编辑器内构建(需在编辑器 Python 环境中运行)
# 方式 A: 在 Output Log Python 控制台:
# exec(open(r"tools/level-builder/main.py").read())
# main(["build", "--input", "output/level_v01.json"])
# 方式 B: File > Execute Python Script
# Stage 3: 生成报告
python main.py report --input output/level_v01.json [--build-log output/build_log.json] --output report.md
"""
import argparse
import json
import logging
import sys
from pathlib import Path
# 将项目 tools 目录加入 sys.path
_TOOLS_DIR = Path(__file__).resolve().parent
_LEVEL_BUILDER_DIR = _TOOLS_DIR
_PROJECT_ROOT = _TOOLS_DIR.parent.parent
if str(_TOOLS_DIR) not in sys.path:
sys.path.insert(0, str(_TOOLS_DIR))
if str(_PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(_PROJECT_ROOT))
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="[%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger("level_builder")
def cmd_parse(args):
"""解析搭建指南 Markdown → JSON。"""
from parsers.build_guide_parser import BuildGuideParser
input_path = args.input
output_path = args.output or "output/level_data.json"
logger.info(f"解析搭建指南: {input_path}")
parser = BuildGuideParser()
data = parser.parse_file(input_path)
# 写入 JSON
out_path = Path(output_path)
out_path.parent.mkdir(parents=True, exist_ok=True)
with open(out_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
summary = data.get("summary", {})
logger.info(f"解析完成: {summary.get('total_actors', 0)} 个 Actor")
logger.info(f" 按类别: {summary.get('by_category', {})}")
logger.info(f" 按区域: {summary.get('by_zone', {})}")
logger.info(f" 唯一资产数: {summary.get('unique_assets', 0)}")
logger.info(f"输出: {out_path.resolve()}")
def cmd_validate(args):
"""验证 JSON 数据中的资产引用。"""
from validation.asset_validator import validate_actors
input_path = args.input
project_root = args.project_root or str(_PROJECT_ROOT)
logger.info(f"验证: {input_path}")
with open(input_path, "r", encoding="utf-8") as f:
data = json.load(f)
actors = data.get("actors", [])
result = validate_actors(actors, project_root)
if result.is_valid:
logger.info("✅ 验证通过!")
else:
logger.error(f"❌ 验证失败: {len(result.errors)} 个错误")
for err in result.errors:
logger.error(f" {err}")
if result.warnings:
for warn in result.warnings:
logger.warning(f" {warn}")
logger.info(f"有效 Actor: {result.valid_actors}/{result.total_actors}")
# 保存验证结果
output_path = Path(input_path).parent / "validation_result.json"
with open(output_path, "w", encoding="utf-8") as f:
json.dump({
"is_valid": result.is_valid,
"total": result.total_actors,
"valid": result.valid_actors,
"errors": result.errors,
"warnings": result.warnings,
"missing_assets": sorted(result.missing_assets),
"missing_materials": sorted(result.missing_materials),
}, f, ensure_ascii=False, indent=2)
logger.info(f"验证结果: {output_path}")
def cmd_build(args):
"""在 UE5 编辑器中构建关卡。"""
try:
from builder.scene_builder import build_level
except ImportError:
print("[错误] 无法导入 builder 模块。确保在 UE5 编辑器 Python 环境中运行。")
print(" 在 Output Log 中切换到 Python,然后执行:")
print(f' exec(open(r"{Path(__file__).resolve()}").read())')
print(f' build_level(r"{Path(args.input).resolve()}")')
return
zones = args.zones.split(",") if args.zones else None
build_level(args.input, clear_existing=args.clear, zones=zones)
def cmd_report(args):
"""生成搭建报告。"""
from report.report_generator import generate_report
input_path = args.input
output_path = args.output or "output/搭建报告.md"
build_log_path = args.build_log
logger.info(f"生成报告: {input_path}")
with open(input_path, "r", encoding="utf-8") as f:
data = json.load(f)
build_log = None
if build_log_path:
try:
with open(build_log_path, "r", encoding="utf-8") as f:
build_log = json.load(f)
except FileNotFoundError:
logger.warning(f"构建日志不存在: {build_log_path},生成无状态报告")
report = generate_report(data, build_log, output_path)
logger.info(f"报告已生成: {output_path}")
def main(argv=None):
"""CLI 入口。"""
parser = argparse.ArgumentParser(
description="ChildHood 白模关卡自动生成工具",
prog="level-builder",
)
subparsers = parser.add_subparsers(dest="command", help="子命令")
# parse
p_parse = subparsers.add_parser("parse", help="解析搭建指南 Markdown → JSON")
p_parse.add_argument("--input", "-i", required=True, help="搭建指南 Markdown 文件路径")
p_parse.add_argument("--output", "-o", default="output/level_data.json", help="输出 JSON 路径")
# validate
p_validate = subparsers.add_parser("validate", help="验证 JSON 数据")
p_validate.add_argument("--input", "-i", required=True, help="JSON 文件路径")
p_validate.add_argument("--project-root", "-r", help="项目根目录(用于文件存在性检查)")
# build
p_build = subparsers.add_parser("build", help="在 UE5 中构建关卡")
p_build.add_argument("--input", "-i", required=True, help="JSON 文件路径")
p_build.add_argument("--clear", action="store_true", default=True, help="清除已有 LVL_* Actor")
p_build.add_argument("--no-clear", dest="clear", action="store_false", help="不清除已有 Actor")
p_build.add_argument("--zones", "-z", help="仅构建指定区域,逗号分隔(如 Z1,Z3)")
# report
p_report = subparsers.add_parser("report", help="生成搭建报告")
p_report.add_argument("--input", "-i", required=True, help="JSON 文件路径")
p_report.add_argument("--build-log", "-l", help="构建日志 JSON 路径")
p_report.add_argument("--output", "-o", default="output/搭建报告.md", help="报告输出路径")
args = parser.parse_args(argv)
if args.command == "parse":
cmd_parse(args)
elif args.command == "validate":
cmd_validate(args)
elif args.command == "build":
cmd_build(args)
elif args.command == "report":
cmd_report(args)
else:
parser.print_help()
if __name__ == "__main__":
main()