70 lines
3.0 KiB
Python
70 lines
3.0 KiB
Python
"""Extract Naninovel DefaultUI TextAsset from Tomie into this workspace.
|
|
|
|
Only reads the game. Writes a JSON translation input and a source manifest.
|
|
Requires: python -m pip install -e ".[unity]"
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
|
|
|
|
def parse_dictionary(text):
|
|
result = []
|
|
seen = set()
|
|
for line_number, line in enumerate(text.splitlines(), 1):
|
|
if not line.strip() or line.lstrip().startswith((";", "#", "//")):
|
|
continue
|
|
key, separator, value = line.partition(":")
|
|
if not separator or not key.strip() or not value.strip():
|
|
continue
|
|
key = key.strip()
|
|
if key in seen:
|
|
raise ValueError(f"Duplicate dictionary key at line {line_number}: {key}")
|
|
seen.add(key)
|
|
result.append({"id": key, "text": value.lstrip(), "line": line_number})
|
|
return result
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("game", type=Path)
|
|
parser.add_argument("--output", type=Path, default=Path("data/tomie/input.json"))
|
|
parser.add_argument("--limit", type=int, default=15, help="0 extracts the full DefaultUI dictionary")
|
|
args = parser.parse_args()
|
|
if args.limit < 0:
|
|
parser.error("limit must be nonnegative")
|
|
try:
|
|
import UnityPy
|
|
except ImportError:
|
|
parser.error('Install optional extractor: python -m pip install -e ".[unity]"')
|
|
asset_path = args.game.resolve() / "TomieWGM_Data" / "data.unity3d"
|
|
output = args.output.resolve()
|
|
if output.is_relative_to(args.game.resolve()):
|
|
parser.error("Choose an output outside the game directory")
|
|
environment = UnityPy.load(str(asset_path))
|
|
assets = [obj for obj in environment.objects if obj.type.name == "TextAsset" and obj.read().m_Name == "DefaultUI"]
|
|
if len(assets) != 1:
|
|
parser.error(f"Expected exactly one DefaultUI TextAsset, found {len(assets)}")
|
|
obj = assets[0]
|
|
script = obj.read().m_Script
|
|
if isinstance(script, bytes):
|
|
script = script.decode("utf-8-sig")
|
|
entries = parse_dictionary(script)
|
|
selected = entries[:args.limit] if args.limit else entries
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
output.write_text(json.dumps([{"id": row["id"], "text": row["text"]} for row in selected], ensure_ascii=False, indent=2), encoding="utf-8")
|
|
manifest = {
|
|
"game": str(args.game.resolve()), "source_file": str(asset_path), "asset_name": "DefaultUI",
|
|
"path_id": obj.path_id, "serialized_file": obj.assets_file.name,
|
|
"script_sha256": hashlib.sha256(script.encode("utf-8")).hexdigest(),
|
|
"total_entries": len(entries), "selected_entries": selected,
|
|
"note": "Read-only extraction. No patching, font replacement or changes to game files.",
|
|
}
|
|
output.with_suffix(".manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
print(f"Extracted {len(selected)}/{len(entries)} UI strings to {output}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|