feat: add native Tomie adapter and strict import compatibility checks
This commit is contained in:
1 parent
c8d83b3fe7
commit
57a4edcbfd
45 files changed
+2985
-71
No files matched your search
@@ -0,0 +1,118 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user