wip deepseek
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user