221 lines
7.3 KiB
Python
221 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Extract game knowledge strings from AIAnalyze.cs, expand [MOD:] tags,
|
|
and write mod-specific knowledge files.
|
|
|
|
Usage: python expand_knowledge.py [--cs-path PATH]
|
|
|
|
Outputs:
|
|
knowledge_default.md — base game (vanilla) knowledge
|
|
knowledge_corona.md — Corona mod knowledge
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
import os
|
|
from pathlib import Path
|
|
|
|
|
|
# ── 1. Extract @"" verbatim strings from C# source ─────────────────────────
|
|
|
|
def extract_verbatim_string(text: str, start: int) -> tuple[str, int]:
|
|
"""
|
|
Extract a C# @"" verbatim string starting after '@"'.
|
|
Returns (content, end_position).
|
|
Handles "" as escaped double-quote.
|
|
"""
|
|
chars = []
|
|
i = start
|
|
while i < len(text):
|
|
c = text[i]
|
|
if c == '"':
|
|
# Escaped quote "" → one literal "
|
|
if i + 1 < len(text) and text[i + 1] == '"':
|
|
chars.append('"')
|
|
i += 2
|
|
continue
|
|
# End of verbatim string
|
|
i += 1
|
|
break
|
|
chars.append(c)
|
|
i += 1
|
|
return ''.join(chars), i
|
|
|
|
|
|
def find_strings(cs_path: str) -> dict[str, str]:
|
|
"""Find all known knowledge string variables in the .cs file and return
|
|
{variable_name: content}."""
|
|
with open(cs_path, 'r', encoding='utf-8') as f:
|
|
source = f.read()
|
|
|
|
known_vars = [
|
|
'generalDescriptions',
|
|
'alliedDescriptions',
|
|
'celestialDescriptions',
|
|
'infinityIsleVanilla',
|
|
'infinityIsleCorona',
|
|
]
|
|
|
|
results = {}
|
|
for var in known_vars:
|
|
# Look for: var {name} = @"
|
|
pattern = f'var {var} = @"'
|
|
idx = source.find(pattern)
|
|
if idx == -1:
|
|
print(f'[WARN] Could not find "{var}" in source')
|
|
continue
|
|
content, end = extract_verbatim_string(source, idx + len(pattern))
|
|
# content still has the leading newline from @"\n...
|
|
results[var] = content
|
|
print(f'[OK] {var}: {len(content)} chars')
|
|
|
|
return results
|
|
|
|
|
|
# ── 2. [MOD:] tag expansion ────────────────────────────────────────────────
|
|
|
|
def expand_mod_text(text: str, mod_name: str) -> str:
|
|
"""Expand [MOD:...] and [MOD:NO:...] tags for the given mod.
|
|
Mimics the logic in AIAnalyze.Process()."""
|
|
lines = text.replace('\r', '').split('\n')
|
|
result_lines = []
|
|
skip_next = False
|
|
|
|
for line in lines:
|
|
if skip_next:
|
|
skip_next = False
|
|
continue
|
|
|
|
stripped = line.strip()
|
|
|
|
# Line-level [MOD:xxx] (must be at start of line, possibly with leading spaces)
|
|
# Check for [MOD:NO:xxx] first
|
|
no_match = re.match(r'^(\s*)\[MOD:NO:([^\]]+)\]$', line)
|
|
if no_match:
|
|
indent, denied_mod = no_match.groups()
|
|
if denied_mod.lower() == mod_name.lower():
|
|
# [MOD:NO:corona] when mod is corona → skip content line
|
|
skip_next = True
|
|
# Either way, skip the tag line itself
|
|
continue
|
|
|
|
# Check for [MOD:xxx]
|
|
mod_match = re.match(r'^(\s*)\[MOD:([^\]]+)\]$', line)
|
|
if mod_match:
|
|
indent, entry_mod = mod_match.groups()
|
|
if entry_mod.lower() != mod_name.lower():
|
|
# This mod's content doesn't apply → skip content line
|
|
skip_next = True
|
|
# Skip the tag line itself
|
|
continue
|
|
|
|
# Inline tags: process [MOD:xxx]content[/MOD] and [MOD:NO:xxx]content[/MOD]
|
|
# Multiple inline tags can appear on one line (e.g. line 662)
|
|
processed = line
|
|
while True:
|
|
# Find next [MOD: or [MOD:NO:
|
|
tag_match = re.search(
|
|
r'\[MOD:(NO:)?([^\]]+)\](.*?)\[/MOD\]',
|
|
processed,
|
|
re.IGNORECASE
|
|
)
|
|
if not tag_match:
|
|
break
|
|
|
|
is_no = tag_match.group(1) is not None
|
|
entry_mod = tag_match.group(2)
|
|
inner_text = tag_match.group(3)
|
|
before = processed[:tag_match.start()]
|
|
after = processed[tag_match.end():]
|
|
|
|
include = False
|
|
if is_no:
|
|
# [MOD:NO:corona] → include if mod != corona
|
|
if entry_mod.lower() != mod_name.lower():
|
|
include = True
|
|
else:
|
|
# [MOD:corona] → include if mod == corona
|
|
if entry_mod.lower() == mod_name.lower():
|
|
include = True
|
|
|
|
if include:
|
|
processed = before + inner_text + after
|
|
else:
|
|
processed = before + after
|
|
|
|
result_lines.append(processed)
|
|
|
|
return '\n'.join(result_lines)
|
|
|
|
|
|
# ── 3. Assemble per-mod knowledge ──────────────────────────────────────────
|
|
|
|
def assemble_knowledge(strings: dict[str, str], mod_name: str) -> str:
|
|
"""Assemble the full knowledge text for a given mod."""
|
|
parts = []
|
|
|
|
# generalDescriptions — most is global rules, but has one inline [MOD:CORONA] tag
|
|
# on the faction list line; process it to expand that tag.
|
|
parts.append(expand_mod_text(strings.get('generalDescriptions', ''), mod_name).strip())
|
|
|
|
# alliedDescriptions
|
|
allied = strings.get('alliedDescriptions', '')
|
|
parts.append(expand_mod_text(allied, mod_name).strip())
|
|
|
|
# celestialDescriptions
|
|
celestial = strings.get('celestialDescriptions', '')
|
|
parts.append(expand_mod_text(celestial, mod_name).strip())
|
|
|
|
# Map description — mod-specific
|
|
map_key = f'infinityIsle{mod_name.capitalize()}'
|
|
if map_key in strings:
|
|
parts.append(strings[map_key].strip())
|
|
else:
|
|
# Fallback: default map
|
|
default_map = strings.get('infinityIsleVanilla', '')
|
|
parts.append(expand_mod_text(default_map, mod_name).strip())
|
|
|
|
return '\n\n\n'.join(p for p in parts if p)
|
|
|
|
|
|
# ── 4. Main ────────────────────────────────────────────────────────────────
|
|
|
|
def main():
|
|
# Determine paths
|
|
script_dir = Path(__file__).resolve().parent
|
|
repo_root = script_dir.parent # tools/ is directly under repo root
|
|
cs_path = repo_root / 'Utils' / 'AIAnalyze.cs'
|
|
output_dir = repo_root
|
|
|
|
# Override via CLI
|
|
if '--cs-path' in sys.argv:
|
|
idx = sys.argv.index('--cs-path')
|
|
if idx + 1 < len(sys.argv):
|
|
cs_path = Path(sys.argv[idx + 1])
|
|
|
|
print(f'Reading: {cs_path}')
|
|
if not cs_path.exists():
|
|
print(f'[ERROR] File not found: {cs_path}')
|
|
sys.exit(1)
|
|
|
|
# Extract all strings
|
|
strings = find_strings(str(cs_path))
|
|
if not strings:
|
|
print('[ERROR] No strings extracted, aborting.')
|
|
sys.exit(1)
|
|
|
|
# Generate per-mod files
|
|
for mod_name in ('default', 'corona'):
|
|
knowledge = assemble_knowledge(strings, mod_name)
|
|
out_path = output_dir / f'knowledge_{mod_name}.md'
|
|
with open(out_path, 'w', encoding='utf-8') as f:
|
|
f.write(knowledge)
|
|
lines = knowledge.count('\n') + 1
|
|
print(f'[OK] {out_path.name}: {lines} lines, {len(knowledge)} chars')
|
|
|
|
print('\nDone. Files written to:', output_dir)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|