feat:优化职业技能关联实现
This commit was merged in pull request #11.
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
"""从 skill_tree.json 生成职业技能引用及中文悬停说明。
|
||||
|
||||
默认读取仓库内中文技能树;--source-root 可指向 dnfSkillData/openapi。
|
||||
只生成标准引用,不改技能计算代码,也不为未知技能猜测映射。
|
||||
使用 --check 校验已提交产物是否与输入一致;校验模式不写文件。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE_ROOT = ROOT / 'backend/openapi/static/data/cn'
|
||||
OUTPUT_ROOT = ROOT / 'backend/core/character/refs'
|
||||
MARKER = '# 此文件由 scripts/generate_skill_refs.py 自动生成,请勿手工修改。'
|
||||
|
||||
# 显式公共技能清单;各职业输入必须包含这些 code,且名称一致。
|
||||
COMMON_SKILLS = {
|
||||
'physicalcriticalhitup': '暴击',
|
||||
'backstep': '后跳',
|
||||
'basicattackup': '基础精通',
|
||||
'escapebackstep': '强化 - 后跳',
|
||||
'jumppowerup': '跃翔',
|
||||
'quickstanding': '受身蹲伏',
|
||||
'armormasteryetc': '防具精通',
|
||||
}
|
||||
|
||||
# 两项觉醒技能按原始 code 后缀分组,所有定义集中在 common.py。
|
||||
# 只允许明确确认的名称差异,不用模糊名称推测 code。
|
||||
AWAKENING_SKILLS = {
|
||||
str(index): {
|
||||
f'clueofawakening{index}': ('觉醒之抉择', '觉醒之心') if index == 1 else ('觉醒之抉择',),
|
||||
f'instantrealization{index}': ('卓越之力',),
|
||||
}
|
||||
for index in range(1, 6)
|
||||
}
|
||||
COMBO_SKILLS = {'skillpresettype1': '技能连招'}
|
||||
COMBO_EXCLUDED_PROFESSIONS = {'extra.creator', 'extra.dark_knight'}
|
||||
|
||||
|
||||
def render_common() -> str:
|
||||
"""集中生成基础、觉醒变体及连招引用,职业按实际技能树继承适用分组。"""
|
||||
lines = [MARKER, '"""公共技能引用;保留觉醒 code 变体,按职业技能树选择继承分组。"""',
|
||||
'from core.character.refs.base import SkillRef, SkillReferences', '', '',
|
||||
'class CommonSkillRefs(SkillReferences):',
|
||||
' """全部职业共享的七项基础引用,仅共享身份,不合并技能计算实现。"""']
|
||||
|
||||
def append_members(skills):
|
||||
for code, label in sorted(skills.items()):
|
||||
lines.extend(['', f' {member_name(code)} = SkillRef(name={label!r}, code={code!r})',
|
||||
' ' + json.dumps(label, ensure_ascii=False)])
|
||||
|
||||
append_members(COMMON_SKILLS)
|
||||
for suffix, skills in AWAKENING_SKILLS.items():
|
||||
lines.extend(['', '', f'class Awakening{suffix}SkillRefs(SkillReferences):',
|
||||
f' """觉醒公共技能第 {suffix} 组;后缀沿用职业技能树原始 code。"""'])
|
||||
append_members({code: ' / '.join(labels) for code, labels in skills.items()})
|
||||
lines.extend(['', '', 'class ComboSkillRefs(SkillReferences):',
|
||||
' """拥有技能连招的职业共享的引用;创造者、黑暗武士不继承。"""'])
|
||||
append_members(COMBO_SKILLS)
|
||||
return '\n'.join(lines) + '\n'
|
||||
|
||||
|
||||
def common_groups(entries: dict, profession: str) -> tuple[list[str], set[str]]:
|
||||
"""校验公共技能覆盖和名称,返回适用继承分组及本职业的公共 code。"""
|
||||
codes = {code: label for code, label in entries.values()}
|
||||
for code, label in COMMON_SKILLS.items():
|
||||
if code not in codes:
|
||||
raise ValueError(f'{profession}: 缺少公共技能 code: {code}')
|
||||
if codes[code] != label:
|
||||
raise ValueError(f'{profession}: 公共技能名称不一致: {code}: {codes[code]!r} != {label!r}')
|
||||
groups = ['CommonSkillRefs']
|
||||
shared = set(COMMON_SKILLS)
|
||||
variants = set().union(*(set(skills) for skills in AWAKENING_SKILLS.values()))
|
||||
selected = variants.intersection(codes)
|
||||
matching = [suffix for suffix, skills in AWAKENING_SKILLS.items() if set(skills) == selected]
|
||||
if len(matching) != 1:
|
||||
raise ValueError(f'{profession}: 觉醒公共技能必须为一组完整且唯一的 code: {sorted(selected)}')
|
||||
suffix = matching[0]
|
||||
for code, labels in AWAKENING_SKILLS[suffix].items():
|
||||
if codes[code] not in labels:
|
||||
raise ValueError(f'{profession}: 觉醒公共技能名称不一致: {code}: {codes[code]!r}')
|
||||
groups.append(f'Awakening{suffix}SkillRefs')
|
||||
shared.update(selected)
|
||||
for code, label in COMBO_SKILLS.items():
|
||||
expected = profession not in COMBO_EXCLUDED_PROFESSIONS
|
||||
if (code in codes) != expected:
|
||||
raise ValueError(f'{profession}: 技能连招覆盖与公共清单不一致: {code}')
|
||||
if expected:
|
||||
if codes[code] != label:
|
||||
raise ValueError(f'{profession}: 公共技能名称不一致: {code}')
|
||||
groups.append('ComboSkillRefs')
|
||||
shared.add(code)
|
||||
return groups, shared
|
||||
|
||||
|
||||
def member_name(code: str) -> str:
|
||||
"""规范化 Python 成员名,原始 code 值保持不变;数字开头时添加 SKILL_。"""
|
||||
if not isinstance(code, str) or not re.fullmatch(r'[A-Za-z0-9_\-]+', code):
|
||||
raise ValueError(f'无效技能 code: {code!r}')
|
||||
name = code.replace('-', '_').upper()
|
||||
if not name or name.startswith('_'):
|
||||
raise ValueError(f'无效技能成员名: {code!r}')
|
||||
return f'SKILL_{name}' if name[0].isdigit() else name
|
||||
|
||||
|
||||
def render_tree(path: Path, relative: Path) -> str:
|
||||
"""校验单职业技能树并渲染引用文件,重复 code 或成员名均报错。"""
|
||||
job, profession, _ = relative.parts
|
||||
if any(not re.fullmatch(r'[a-z][a-z0-9_]*', part) for part in (job, profession)):
|
||||
raise ValueError(f'无效职业目录: {relative}')
|
||||
rows = json.loads(path.read_text(encoding='utf-8'))
|
||||
if not isinstance(rows, list) or not rows:
|
||||
raise ValueError(f'{relative}: 技能树必须是非空数组')
|
||||
entries = {}
|
||||
codes = set()
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
raise ValueError(f'{relative}: 技能记录必须是对象')
|
||||
code, label = row.get('code'), row.get('name')
|
||||
if not isinstance(label, str) or not label.strip():
|
||||
raise ValueError(f'{relative}: 技能缺少中文名称: {code!r}')
|
||||
member = member_name(code)
|
||||
if code in codes:
|
||||
raise ValueError(f'{relative}: 重复 code: {code}')
|
||||
if member in entries:
|
||||
raise ValueError(f'{relative}: 成员名冲突: {entries[member][0]} / {code} -> {member}')
|
||||
codes.add(code)
|
||||
entries[member] = (code, label)
|
||||
groups, shared = common_groups(entries, f'{job}.{profession}')
|
||||
common_members = {member_name(code) for code in shared}
|
||||
class_name = ''.join(part.capitalize() for part in profession.split('_')) + 'SkillRef'
|
||||
lines = [
|
||||
MARKER,
|
||||
f'"""技能引用来源:{relative.as_posix()};公共技能继承自 common.py 中的适用分组。"""',
|
||||
'from core.character.refs.base import SkillRef',
|
||||
'from core.character.refs.common import ' + ', '.join(groups),
|
||||
'', '',
|
||||
f'class {class_name}({", ".join(groups)}):',
|
||||
f' """{job}.{profession} 的技能引用;包含继承的公共技能。"""',
|
||||
f" __profession__ = '{job}.{profession}'",
|
||||
]
|
||||
for member, (code, label) in sorted(entries.items()):
|
||||
if code in shared:
|
||||
continue
|
||||
if member in common_members:
|
||||
raise ValueError(f'{relative}: 职业成员覆盖公共引用: {member}')
|
||||
lines += ['', f' {member} = SkillRef(name={label!r}, code={code!r}, profession=__profession__)',
|
||||
' ' + json.dumps(label, ensure_ascii=False)]
|
||||
return '\n'.join(lines) + '\n'
|
||||
|
||||
|
||||
def expected_files(source_root: Path) -> dict[Path, str]:
|
||||
"""先校验全部输入,再返回期望产物,避免输入错误造成部分生成。"""
|
||||
trees = sorted(source_root.glob('*/*/skill_tree.json'))
|
||||
if not trees:
|
||||
raise ValueError(f'未找到职业技能树: {source_root}')
|
||||
files = {Path('common.py'): render_common()}
|
||||
for tree in trees:
|
||||
relative = tree.relative_to(source_root)
|
||||
job, profession, _ = relative.parts
|
||||
files[Path(job) / f'{profession}.py'] = render_tree(tree, relative)
|
||||
files[Path(job) / '__init__.py'] = MARKER + '\n' + '"""职业技能引用分组;按需导入具体职业。"""\n'
|
||||
return files
|
||||
|
||||
|
||||
def generate(source_root: Path, output_root: Path, *, check: bool = False) -> list[str]:
|
||||
"""生成或只读检查引用;返回差异路径,不覆盖手工文件或自动删除旧职业。"""
|
||||
expected = expected_files(source_root)
|
||||
stale = [
|
||||
path.relative_to(output_root) for path in sorted(output_root.rglob('*.py'))
|
||||
if path.read_text(encoding='utf-8').startswith(MARKER) and path.relative_to(output_root) not in expected
|
||||
]
|
||||
if stale:
|
||||
raise ValueError('存在来源已移除的生成文件,请核对后清理: ' + ', '.join(map(str, stale)))
|
||||
changes = []
|
||||
for relative, content in expected.items():
|
||||
path = output_root / relative
|
||||
if path.exists():
|
||||
existing = path.read_text(encoding='utf-8')
|
||||
if not existing.startswith(MARKER):
|
||||
raise ValueError(f'拒绝覆盖手工文件: {path}')
|
||||
if existing == content:
|
||||
continue
|
||||
changes.append(str(relative))
|
||||
if not check:
|
||||
for relative in changes:
|
||||
path = output_root / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(expected[Path(relative)], encoding='utf-8')
|
||||
return changes
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""命令行入口;--check 发现差异或输入校验失败时返回非零退出码。"""
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--source-root', type=Path, default=SOURCE_ROOT, help='包含职业技能树的目录')
|
||||
parser.add_argument('--output-root', type=Path, default=OUTPUT_ROOT, help='生成引用文件的目录')
|
||||
parser.add_argument('--check', action='store_true', help='只检查差异,不写文件')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
changes = generate(args.source_root, args.output_root, check=args.check)
|
||||
except (ValueError, OSError) as exc:
|
||||
print(f'生成失败: {exc}', file=sys.stderr)
|
||||
return 1
|
||||
if args.check and changes:
|
||||
print('引用文件需要更新:\n' + '\n'.join(changes))
|
||||
return 1
|
||||
print(f'{"检查通过" if args.check else "生成完成"},差异文件数: {len(changes)}')
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Merge OpenAPI-generated layouts into runtime character modules.
|
||||
"""将生成数据合并到已有职业实现。
|
||||
|
||||
Skills are matched strictly by their constant ``name``. Runtime Skill class
|
||||
identifiers and implementations are preserved; ``position`` and ``cellPos``
|
||||
come from the generated class. Generated-only skills are retained as commented blocks.
|
||||
The runtime header and profession-specific footer are preserved unchanged.
|
||||
优先按职业 ref、其次 UUID 匹配;缺少稳定身份时才兼容旧中文名称。
|
||||
保留运行时类编号、名称、关联规则和手写方法;旧技能缺少 ref 时补齐绑定及导入。
|
||||
生成器独有技能保持注释,运行时独有技能原样保留;职业变体按 prefix 查找来源。
|
||||
|
||||
先执行 backend/openapi/scripts/generate.py 同步 refs 和生成数据,再运行本脚本。
|
||||
--check 只检查合并及语法,不写职业文件;--generated-root/--character-root 可指定验证目录。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -30,6 +32,9 @@ class SkillBlock:
|
||||
end: int
|
||||
position: str | None
|
||||
cell_pos: str | None
|
||||
ref: str | None
|
||||
ref_key: tuple[str, str, str] | None
|
||||
uuid: str | None
|
||||
|
||||
|
||||
def _offsets(source: str) -> list[int]:
|
||||
@@ -42,6 +47,12 @@ def _offsets(source: str) -> list[int]:
|
||||
def _skills(source: str, path: Path) -> list[SkillBlock]:
|
||||
tree = ast.parse(source, filename=str(path))
|
||||
offsets = _offsets(source)
|
||||
imports = {
|
||||
item.asname or item.name: (node.module, item.name)
|
||||
for node in tree.body if isinstance(node, ast.ImportFrom)
|
||||
and (node.module or '').startswith('core.character.refs.')
|
||||
for item in node.names
|
||||
}
|
||||
result: list[SkillBlock] = []
|
||||
for node in tree.body:
|
||||
if not isinstance(node, ast.ClassDef) or not re.fullmatch(r"Skill\d+", node.name):
|
||||
@@ -49,12 +60,21 @@ def _skills(source: str, path: Path) -> list[SkillBlock]:
|
||||
name = None
|
||||
position = None
|
||||
cell_pos = None
|
||||
ref = ref_key = uuid = None
|
||||
for statement in node.body:
|
||||
if not isinstance(statement, (ast.Assign, ast.AnnAssign)):
|
||||
continue
|
||||
targets = statement.targets if isinstance(statement, ast.Assign) else [statement.target]
|
||||
if len(targets) != 1 or not isinstance(targets[0], ast.Name):
|
||||
continue
|
||||
if targets[0].id == 'uuid' and isinstance(statement.value, ast.Constant):
|
||||
uuid = statement.value.value
|
||||
if targets[0].id == 'ref':
|
||||
value = statement.value
|
||||
if not isinstance(value, ast.Attribute) or not isinstance(value.value, ast.Name) or value.value.id not in imports:
|
||||
raise ValueError(f'{path}: 技能 ref 必须引用已导入的职业 Ref 成员')
|
||||
ref = ast.unparse(value)
|
||||
ref_key = (*imports[value.value.id], value.attr)
|
||||
if targets[0].id == "name" and isinstance(statement.value, ast.Constant):
|
||||
name = statement.value.value
|
||||
elif targets[0].id == "position":
|
||||
@@ -71,6 +91,7 @@ def _skills(source: str, path: Path) -> list[SkillBlock]:
|
||||
offsets[node.end_lineno],
|
||||
position,
|
||||
cell_pos,
|
||||
ref, ref_key, uuid,
|
||||
)
|
||||
)
|
||||
return result
|
||||
@@ -458,11 +479,72 @@ def _sync_module_assignment(
|
||||
|
||||
|
||||
def _comment(block: str, name: str) -> str:
|
||||
lines = [f"# MIGRATION: character 中没有同名技能“{name}”,暂不启用\n"]
|
||||
lines = [f"# MIGRATION: character 中没有匹配身份的技能“{name}”,暂不启用\n"]
|
||||
lines.extend("# " + line.rstrip() + "\n" if line.strip() else "#\n" for line in block.splitlines())
|
||||
return "".join(lines)
|
||||
|
||||
|
||||
def _identity_indexes(skills, path):
|
||||
"""校验稳定身份唯一性,禁止重复引用或 UUID 被字典静默覆盖。"""
|
||||
indexes = {}
|
||||
for field in ('ref_key', 'uuid', 'skill_name'):
|
||||
index = {}
|
||||
for skill in skills:
|
||||
value = getattr(skill, field)
|
||||
if value is None:
|
||||
continue
|
||||
if value in index:
|
||||
raise ValueError(f'{path}: duplicate {field}: {value}')
|
||||
index[value] = skill
|
||||
indexes[field] = index
|
||||
return indexes
|
||||
|
||||
|
||||
def _match_skill(skill, indexes):
|
||||
"""优先 ref、其次 UUID;仅在缺少可比较身份时兼容旧中文名称。"""
|
||||
by_ref = indexes['ref_key'].get(skill.ref_key)
|
||||
by_uuid = indexes['uuid'].get(skill.uuid)
|
||||
if by_ref is not None and by_uuid is not None and by_ref is not by_uuid:
|
||||
raise ValueError(f'技能 ref 与 UUID 指向不同目标: {skill.skill_name}')
|
||||
candidate = by_ref or by_uuid or indexes['skill_name'].get(skill.skill_name)
|
||||
if candidate is None:
|
||||
return None
|
||||
if skill.ref_key and candidate.ref_key and skill.ref_key != candidate.ref_key:
|
||||
raise ValueError(f'技能引用冲突: {skill.skill_name}: {candidate.ref} -> {skill.ref}')
|
||||
if by_ref is None and skill.uuid and candidate.uuid and skill.uuid != candidate.uuid:
|
||||
raise ValueError(f'同名技能 UUID 冲突: {skill.skill_name}')
|
||||
return candidate
|
||||
|
||||
|
||||
def _ensure_ref_imports(source, generated):
|
||||
"""补齐生成代码的职业 Ref 导入,保留原有别名,拒绝别名指向不同对象。"""
|
||||
tree = ast.parse(source)
|
||||
imports = {
|
||||
item.asname or item.name: (node.module, item.name)
|
||||
for node in tree.body if isinstance(node, ast.ImportFrom) for item in node.names
|
||||
}
|
||||
additions = []
|
||||
for node in ast.parse(generated).body:
|
||||
if not isinstance(node, ast.ImportFrom) or not (node.module or '').startswith('core.character.refs.'):
|
||||
continue
|
||||
for item in node.names:
|
||||
alias = item.asname or item.name
|
||||
if alias in imports:
|
||||
if imports[alias] != (node.module, item.name):
|
||||
raise ValueError(f'职业引用导入别名冲突: {alias}')
|
||||
else:
|
||||
additions.append(f'from {node.module} import {item.name}' + (f' as {item.asname}' if item.asname else '') + '\n')
|
||||
imports[alias] = (node.module, item.name)
|
||||
if not additions:
|
||||
return source
|
||||
position = max((node.end_lineno for node in tree.body if isinstance(node, (ast.Import, ast.ImportFrom))), default=0)
|
||||
if position == 0:
|
||||
return ''.join(additions) + '\n' + source.lstrip('\r\n')
|
||||
lines = source.splitlines(keepends=True)
|
||||
lines[position:position] = additions
|
||||
return ''.join(lines)
|
||||
|
||||
|
||||
def merge(generated_path: Path, character_path: Path) -> tuple[str, dict[str, int]]:
|
||||
generated = generated_path.read_text(encoding="utf-8")
|
||||
character = character_path.read_text(encoding="utf-8")
|
||||
@@ -470,9 +552,9 @@ def merge(generated_path: Path, character_path: Path) -> tuple[str, dict[str, in
|
||||
character_skills = _skills(character, character_path)
|
||||
if not generated_skills or not character_skills:
|
||||
raise ValueError(f"missing skills: {generated_path} or {character_path}")
|
||||
runtime = {skill.skill_name: skill for skill in character_skills}
|
||||
if len(runtime) != len(character_skills):
|
||||
raise ValueError(f"duplicate skill name: {character_path}")
|
||||
runtime = _identity_indexes(character_skills, character_path)
|
||||
_identity_indexes(generated_skills, generated_path)
|
||||
used = set()
|
||||
|
||||
character_header_end, character_footer_start = _module_skill_bounds(character, character_path)
|
||||
generated_header_end, _ = _module_skill_bounds(generated, generated_path)
|
||||
@@ -483,13 +565,21 @@ def merge(generated_path: Path, character_path: Path) -> tuple[str, dict[str, in
|
||||
for index, generated_skill in enumerate(generated_skills):
|
||||
if index:
|
||||
chunks.append(generated[cursor:generated_skill.start])
|
||||
runtime_skill = runtime.get(generated_skill.skill_name)
|
||||
runtime_skill = _match_skill(generated_skill, runtime)
|
||||
if runtime_skill is None:
|
||||
chunks.append(_comment(generated[generated_skill.start:generated_skill.end], generated_skill.skill_name))
|
||||
commented += 1
|
||||
else:
|
||||
if runtime_skill.class_name in used:
|
||||
raise ValueError(f'多个生成技能匹配同一运行时技能: {runtime_skill.class_name}')
|
||||
used.add(runtime_skill.class_name)
|
||||
block = character[runtime_skill.start:runtime_skill.end]
|
||||
generated_block = generated[generated_skill.start:generated_skill.end]
|
||||
if generated_skill.ref is not None and runtime_skill.ref is None:
|
||||
block = _set_generated_field(block, 'ref', generated_skill.ref)
|
||||
# 名称保留运行时值,防止破坏未迁移的名称关联;稳定 ref 匹配不要求名称相同。
|
||||
if generated_skill.uuid is not None and generated_skill.ref_key == runtime_skill.ref_key and generated_skill.ref_key is not None:
|
||||
block = _set_generated_field(block, 'uuid', repr(generated_skill.uuid))
|
||||
block = _sync_generated_assignment(block, generated_block, "vps")
|
||||
block = _sync_generated_data(block, generated_block)
|
||||
block = _sync_generated_comments(block, generated_block)
|
||||
@@ -502,11 +592,16 @@ def merge(generated_path: Path, character_path: Path) -> tuple[str, dict[str, in
|
||||
chunks.append(block.rstrip() + "\n")
|
||||
matched += 1
|
||||
cursor = generated_skill.end
|
||||
# 未出现在新生成数据中的手写技能必须保留,不能随布局重建被删除。
|
||||
for skill in character_skills:
|
||||
if skill.class_name not in used:
|
||||
chunks.append('\n' + character[skill.start:skill.end].rstrip() + '\n')
|
||||
chunks.append("\n" + character[character_footer_start:].lstrip("\r\n"))
|
||||
merged = "".join(chunks)
|
||||
merged = "\n".join(line.rstrip() for line in merged.splitlines()) + "\n"
|
||||
merged = _sync_module_assignment(merged, generated, "prefix")
|
||||
merged = _ensure_get_data_import(merged)
|
||||
merged = _ensure_ref_imports(merged, generated)
|
||||
return merged, {
|
||||
"generated": len(generated_skills), "runtime": len(character_skills),
|
||||
"matched": matched, "commented": commented,
|
||||
@@ -545,12 +640,22 @@ def _atomic_write(path: Path, content: str) -> None:
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--check", action="store_true", help="validate without writing")
|
||||
parser.add_argument("--generated-root", type=Path, default=GENERATED_ROOT)
|
||||
parser.add_argument("--character-root", type=Path, default=CHARACTER_ROOT)
|
||||
args = parser.parse_args()
|
||||
totals = {key: 0 for key in ("files", "generated", "runtime", "matched", "commented", "runtime_only")}
|
||||
for generated_path in sorted(GENERATED_ROOT.rglob("*.py")):
|
||||
character_path = CHARACTER_ROOT / generated_path.relative_to(GENERATED_ROOT)
|
||||
if not character_path.exists():
|
||||
print(f"SKIP {generated_path.relative_to(GENERATED_ROOT)}: no runtime profession")
|
||||
for character_path in sorted(args.character_root.rglob("*.py")):
|
||||
if character_path.name == '__init__.py':
|
||||
continue
|
||||
# 使用数据 prefix 找生成来源,输出圣骑士等变体也能复用来源职业。
|
||||
module = ast.parse(character_path.read_text(encoding='utf-8'))
|
||||
prefix = next((node.value.value for node in module.body
|
||||
if _statement_key(node) == 'prefix' and isinstance(node.value, ast.Constant)), None)
|
||||
relative = character_path.relative_to(args.character_root)
|
||||
generated_relative = Path(*prefix.split('/')[:2]).with_suffix('.py') if isinstance(prefix, str) else relative
|
||||
generated_path = args.generated_root / generated_relative
|
||||
if not generated_path.exists():
|
||||
print(f"SKIP {relative}: no generated profession")
|
||||
continue
|
||||
output, stats = merge(generated_path, character_path)
|
||||
ast.parse(output, filename=str(character_path))
|
||||
@@ -559,7 +664,7 @@ def main() -> int:
|
||||
totals["files"] += 1
|
||||
for key, value in stats.items():
|
||||
totals[key] += value
|
||||
print(f"{'CHECK' if args.check else 'WRITE'} {generated_path.relative_to(GENERATED_ROOT)}: "
|
||||
print(f"{'CHECK' if args.check else 'WRITE'} {relative}: "
|
||||
f"matched={stats['matched']} commented={stats['commented']} runtime_only={stats['runtime_only']}")
|
||||
print("TOTAL " + " ".join(f"{key}={value}" for key, value in totals.items()))
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user