"""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