- Rate-limit failed logins (8 per 15 min per IP or username, 429 + Retry-After) - Add ruff/mypy config and GitHub Actions workflow (ruff, pytest, tsc, tests, build) - Apply ruff autofixes (import order, pyupgrade) - Extract rpy_helpers.py, engine-util.ts, TranslationParts.tsx and translation-types.ts from oversized modules - Add dependency-free React UI tests - Refresh README and ARCHITECTURE (Naninovel/Tomie, status and plans) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
129 lines
6.0 KiB
Python
129 lines
6.0 KiB
Python
"""Strict codec for Unity 2019 managed-reference scenario objects.
|
|
|
|
Schemas come from the game's own assemblies. The old version-1 registry uses
|
|
implicit integer IDs and a Terminus record, rather than the modern array format.
|
|
Use in an isolated asset-processing process; UnityPy's codec hooks are global.
|
|
"""
|
|
from contextlib import contextmanager
|
|
|
|
|
|
class ScriptCodec:
|
|
def __init__(self, game, version="2019.4.22f1"):
|
|
from UnityPy.helpers.TypeTreeGenerator import TypeTreeGenerator
|
|
self.generator = TypeTreeGenerator(version)
|
|
self.generator.load_local_game(str(game))
|
|
self.cache = {}
|
|
self.templates = {}
|
|
|
|
def node(self, assembly, fullname, reference=False):
|
|
from UnityPy.helpers import TypeTreeHelper as helper
|
|
from UnityPy.helpers.TypeTreeNode import TypeTreeNode
|
|
key = (assembly, fullname, reference)
|
|
if key in self.cache:
|
|
return self.cache[key]
|
|
source = self.generator.get_nodes_up(assembly, fullname)
|
|
# The generator calls List<string> "string"; the generic reader would
|
|
# mistake its count for a scalar string length. Normalize array nodes.
|
|
def clone(n):
|
|
values = n.to_dict()
|
|
values["m_Children"] = [clone(c) for c in n.m_Children]
|
|
return TypeTreeNode(**values)
|
|
node = clone(source)
|
|
for child in node.traverse():
|
|
if (child.m_Children and child.m_Children[0].m_Type == "Array"
|
|
and child.m_Type in helper.FUNCTION_READ_MAP
|
|
and child.m_Children[0].m_Children[1].m_Type != "char"):
|
|
child.m_Type = "vector"
|
|
if reference:
|
|
# Generator assumes MonoBehaviour even for plain serializable classes.
|
|
children = [c for c in node.m_Children if c.m_Name not in
|
|
{"m_GameObject", "m_Enabled", "m_Script", "m_Name", "references"}]
|
|
node = TypeTreeNode(0, node.m_Type, node.m_Name, -1, 0, m_Children=children)
|
|
self.cache[key] = node
|
|
return node
|
|
|
|
def ref_node(self, typ):
|
|
if not typ["class"]:
|
|
return None
|
|
if not typ["asm"] or not all(ord(c) >= 32 for c in typ["class"] + typ["ns"] + typ["asm"]):
|
|
raise ValueError("Invalid managed-reference type; refusing to guess its layout")
|
|
key = (typ["asm"].removesuffix(".dll"), typ["ns"], typ["class"])
|
|
if key in self.templates:
|
|
return self.templates[key]
|
|
return self.node(typ["asm"], ".".join(filter(None, [typ["ns"], typ["class"]])), True)
|
|
|
|
@contextmanager
|
|
def active(self):
|
|
from UnityPy.helpers import TypeTreeHelper as helper
|
|
old_read, old_write = helper.read_value, helper.write_value
|
|
old_boost, old_resolve = helper.read_typetree_boost, helper.get_ref_type_node
|
|
|
|
def read(node, reader, config):
|
|
if node.m_Type != "ManagedReferencesRegistry":
|
|
return old_read(node, reader, config)
|
|
version = reader.read_int()
|
|
if version != 1:
|
|
raise ValueError(f"Unsupported managed-reference registry: {version}")
|
|
type_node = node.m_Children[1].m_Children[0]
|
|
records = []
|
|
while len(records) < 1_000_000:
|
|
typ = old_read(type_node, reader, config)
|
|
if typ == {"class": "Terminus", "ns": "UnityEngine.DMAT", "asm": "FAKE_ASM"}:
|
|
return {"version": version, "records": records, "terminus": typ}
|
|
schema = self.ref_node(typ)
|
|
records.append({"type": typ, "data": read(schema, reader, config) if schema else {}})
|
|
raise ValueError("Unterminated managed-reference registry")
|
|
|
|
def write(value, node, writer, config):
|
|
if node.m_Type != "ManagedReferencesRegistry":
|
|
return old_write(value, node, writer, config)
|
|
if value["version"] != 1:
|
|
raise ValueError("Unsupported registry version")
|
|
writer.write_int(1)
|
|
type_node = node.m_Children[1].m_Children[0]
|
|
for ref in value["records"]:
|
|
old_write(ref["type"], type_node, writer, config)
|
|
schema = self.ref_node(ref["type"])
|
|
if schema:
|
|
write(ref["data"], schema, writer, config)
|
|
old_write(value["terminus"], type_node, writer, config)
|
|
|
|
helper.read_typetree_boost = None
|
|
helper.get_ref_type_node = lambda ref, _: self.ref_node(ref["type"])
|
|
helper.read_value, helper.write_value = read, write
|
|
try:
|
|
yield self
|
|
finally:
|
|
helper.read_value, helper.write_value = old_read, old_write
|
|
helper.read_typetree_boost, helper.get_ref_type_node = old_boost, old_resolve
|
|
|
|
def read(self, obj):
|
|
from UnityPy.helpers import TypeTreeHelper as helper
|
|
from UnityPy.streams import EndianBinaryWriter
|
|
# Bundles can retain exact schemas which differ from generated metadata
|
|
# (notably editor-compiled Init). Prefer those schemas when available.
|
|
self.templates = {(ref.m_AssemblyName.removesuffix(".dll"), ref.m_NameSpace, ref.m_ClassName): ref.node
|
|
for ref in obj.assets_file.ref_types if ref.node is not None}
|
|
node = obj.serialized_type.node
|
|
if node is None:
|
|
node = self.node("Elringus.Naninovel.Runtime", "Naninovel.Script")
|
|
tree = obj.read_typetree(nodes=node)
|
|
writer = EndianBinaryWriter(endian=obj.reader.endian)
|
|
helper.write_typetree(tree, node, writer, obj.assets_file)
|
|
if writer.bytes != obj.get_raw_data():
|
|
raise ValueError(f"Script {obj.path_id}: binary round-trip mismatch")
|
|
return tree
|
|
|
|
|
|
def script_objects(environment):
|
|
for obj in environment.objects:
|
|
if obj.type.name != "MonoBehaviour":
|
|
continue
|
|
try:
|
|
head = obj.parse_monobehaviour_head()
|
|
script = head.m_Script.read()
|
|
except (ValueError, AttributeError, FileNotFoundError):
|
|
continue
|
|
if script.m_Namespace == "Naninovel" and script.m_ClassName == "Script":
|
|
yield obj
|