Files
pgames-manager/backend/app/converter/tomie_format.py
T

119 lines
6.8 KiB
Python

"""Editable PGVN envelope for the version-pinned native Naninovel interpreter."""
import copy
import hashlib
from app.converter.compatibility import CompatibilityError, Issue
from app.converter.tomie_profile import runtime_data
from app.converter.tomie_validation import validate_runtime
RUNTIME = "tomie/1"
TEXT_FIELDS = {"PrintText": "Text", "AppendText": "Text", "AddChoice": "ChoiceSummary",
"ShowToastUI": "Text", "InputCustomVariable": "Summary"}
def prepare(package):
if package.issues:
raise CompatibilityError("naninovel", package.issues)
try:
data = runtime_data(package)
except ValueError as exc:
raise CompatibilityError("naninovel", [Issue("tomie_metadata", str(exc))]) from exc
issues = validate_runtime(data)
if issues:
raise CompatibilityError("naninovel", issues)
routes = {"19": {"script": "DicePlaySub", "label": "DayEnd"},
"1000": {"script": "TRandbox", "label": "Start"}}
import re
for command in data["scripts"].get("DicePlaySub", {}).get("commands", []):
condition = command["args"].get("ConditionalExpression", "")
match = re.fullmatch(r"thisBlockNumber\s*==\s*(\d+)", condition) if isinstance(condition, str) else None
if command["op"] == "Goto" and match:
path = command["args"]["Path"]
if not isinstance(path, dict) or "template" in path:
raise CompatibilityError("naninovel", [Issue("tomie_board", "Динамический адрес клетки поля", str(command["line"]))])
routes[match[1]] = {"script": path.get("name") or "DicePlaySub", "label": path.get("value") or ""}
data["board_routes"] = routes
# Browser-native message dialog returns to the source's app screen, exactly
# as the two-command bridge in the recovered port does.
data["scripts"]["PGamesMessage"] = {"labels": {}, "label_depths": {}, "commands": [
{"op": "Gosub", "args": {"Path": "MessagePlay"}, "line": -1},
{"op": "Goto", "args": {"Path": "AppScreen"}, "line": -1},
]}
return data
def identity(value):
return "tomie-" + hashlib.sha256(value.encode()).hexdigest()[:24]
def modules(data, texts):
result = {}
for name, script in data["scripts"].items():
body = []
for index, command in enumerate(script["commands"]):
node = {"op": "nani_command", "id": identity(f"{name}:{index}"), "command": copy.deepcopy(command)}
field = TEXT_FIELDS.get(command["op"])
value = command["args"].get(field) if field else None
if value is not None:
node.update(what=value["template"] if isinstance(value, dict) else str(value),
tid=command.get("tid") or identity(f"text:{name}:{index}"), textField=field,
whoStr=str(command["args"].get("AuthorId", "")))
body.append(node)
path = f"script/nani/{identity(name)}.json"
result[path] = {"file": name + ".nani", "labels": [
{"name": "nani:" + name, "id": identity("script:" + name), "body": body}],
"nani": {"name": name, "labels": script["labels"], "label_depths": script.get("label_depths", {}),
"label_ids": {label: body[index]["id"] if index < len(body) else ""
for label, index in script["labels"].items()}}}
resources = []
for tid, record in texts.items():
resources.append({"op": "say", "id": identity(tid), "tid": tid, "what": record["text"],
"whoStr": record["collection"], "resource": copy.deepcopy(record)})
result["script/nani/resources.json"] = {"file": "_resources.nani", "labels": [
{"name": "nani:resources", "id": identity("resources"), "body": resources}]}
return result
def unpack_modules(scripts):
"""Rebuild the VM input from editable commands, including edited prose."""
result = {}
for module in scripts.values():
metadata = module.get("nani")
if metadata is None:
continue
if not isinstance(metadata, dict) or not isinstance(metadata.get("name"), str) or metadata["name"] in result:
raise ValueError("Некорректный модуль Naninovel")
commands, positions = [], {}
for label in module["labels"]:
for node in label["body"]:
if node.get("op") != "nani_command" or not isinstance(node.get("command"), dict):
raise ValueError("Модуль Tomie должен содержать исходные команды Naninovel")
command = copy.deepcopy(node["command"])
positions[node.get("id")] = len(commands)
if not isinstance(command.get("op"), str) or not isinstance(command.get("args"), dict):
raise ValueError("Некорректная команда Naninovel")
field = TEXT_FIELDS.get(command["op"])
if field and field in command["args"] and "what" not in node:
raise ValueError("Отсутствует редактируемый текст команды Naninovel")
if field and "what" in node:
if node.get("textField") != field or not isinstance(node["what"], str):
raise ValueError("Некорректное поле локализации Naninovel")
command["args"][field] = {"template": node["what"]}
command["tid"] = node.get("tid")
commands.append(command)
labels, depths = metadata.get("labels"), metadata.get("label_depths", {})
if not isinstance(labels, dict):
raise ValueError("Некорректные метки Naninovel")
identities = metadata.get("label_ids")
if identities is not None:
if not isinstance(identities, dict) or set(identities) != set(labels or {}) or any(
not isinstance(value, str) or (value and value not in positions) for value in identities.values()):
raise ValueError("Удалена команда, на которую ссылается метка Naninovel")
labels = {key: positions[value] if value else len(commands) for key, value in identities.items()}
if not isinstance(labels, dict) or any(not isinstance(key, str) or type(value) is not int or not 0 <= value <= len(commands) for key, value in labels.items()):
raise ValueError("Некорректный адрес метки Naninovel")
if not isinstance(depths, dict) or any(key not in labels or type(value) is not int or not 0 <= value <= 64 for key, value in depths.items()):
raise ValueError("Некорректная вложенность метки Naninovel")
result[metadata["name"]] = {"commands": commands, "labels": labels, "label_depths": depths}
return result