feat: converter reads the Phone library's register statement, named stores and generators
`init phone register` becomes a default (the group chat) plus a function of library calls that runs with the other defaults of a new game (phone_register.py). `init/python [early|hide] in some.store` wraps its code in an in_store node and `default some.store.name` may name a store; yield works as an expression (generator functions are marked gen); `import x as y`, `transform -150 name(...)`, `init:` with if/else, `config.special_namespaces` lines, `at` lists and tuples, transforms made by Python functions, `choice` weights that are expressions, `__len__`/`__bool__`, and the whitelist for os, calendar, itertools, pygame_sdl2 and the new runtime. The converter also records the size of every picture it converts (`imageSizes`) so `renpy.image_size` answers without loading. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e94f9b0d9e
commit
83cc7fdd5c
11 files changed
+379
-26
No files matched your search
@@ -73,6 +73,7 @@ BUILTIN_CHARACTERS = {"narrator", "centered", "vcentered", "extend", "nvl", "adv
|
||||
|
||||
@dataclass
|
||||
class ScriptData:
|
||||
phone_registers: int = 0 # `init phone register` blocks read so far (they get numbered functions)
|
||||
characters: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
images: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
transforms: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
@@ -468,12 +469,28 @@ class RpyParser:
|
||||
return []
|
||||
if c.keyword("python") or c.keyword("early"):
|
||||
c.keyword("python")
|
||||
self.init_python(line.block_source(), line)
|
||||
c.keyword("hide") # `init python hide in x:` keeps its temporary names out of the store; they are only locals here
|
||||
self.init_python(line.block_source(), line, self.store_name(c))
|
||||
return []
|
||||
rest = c.rest().strip()
|
||||
if re.match(r"phone\s+register(?!\w)", rest): # the Phone library's statement (see phone_register.py)
|
||||
self.init_phone_register(line, rest)
|
||||
return []
|
||||
if rest and rest != ":": # `init -490 screen name(...):` - one statement with a priority in front
|
||||
return self.statement(replace(line, text=rest), top_level=True)
|
||||
for child in self.fold_live2d(line.block):
|
||||
children = self.fold_live2d(line.block)
|
||||
i = 0
|
||||
while i < len(children):
|
||||
child = children[i]
|
||||
if re.match(r"if(?!\w)", child.text.strip()): # `init:` with an if/elif/else of `$` lines
|
||||
branches, i = self.if_chain(children, i)
|
||||
code = self.init_branches(branches)
|
||||
if code is None:
|
||||
self.d.warn("Оператор сцены внутри init", self.where(child))
|
||||
else:
|
||||
self.d.init_code.extend(code)
|
||||
continue
|
||||
i += 1
|
||||
for node in self.statement(child, top_level=True):
|
||||
if node["op"] == "exec":
|
||||
self.d.init_code.extend(node["code"])
|
||||
@@ -481,6 +498,52 @@ class RpyParser:
|
||||
self.d.warn("Оператор сцены внутри init", self.where(child))
|
||||
return []
|
||||
|
||||
def init_branches(self, branches: list[dict[str, Any]]) -> list[Stmt] | None:
|
||||
"""An if/elif/else whose branches hold only Python lines, as one init statement (None when they hold more)."""
|
||||
def body(nodes: list[Node]) -> list[Stmt] | None:
|
||||
out: list[Stmt] = []
|
||||
for node in nodes:
|
||||
if node["op"] == "exec":
|
||||
out.extend(node["code"])
|
||||
elif node["op"] == "if":
|
||||
nested = self.init_branches(node["branches"])
|
||||
if nested is None:
|
||||
return None
|
||||
out.extend(nested)
|
||||
elif node["op"] != "window":
|
||||
return None
|
||||
return out
|
||||
|
||||
chain: dict[str, Any] | None = None
|
||||
for branch in reversed(branches):
|
||||
inner = body(branch["body"])
|
||||
if inner is None:
|
||||
return None
|
||||
if branch["c"] is None:
|
||||
chain = {"op": "if", "c": {"k": "const", "v": True}, "then": inner, "else": []}
|
||||
else:
|
||||
chain = {"op": "if", "c": branch["c"], "then": inner, "else": [chain] if chain else []}
|
||||
return [chain] if chain else []
|
||||
|
||||
def init_phone_register(self, line: Line, rest: str) -> None:
|
||||
from app.converter.phone_register import PhoneSyntax, Register
|
||||
|
||||
self.d.phone_registers += 1
|
||||
number = self.d.phone_registers
|
||||
try:
|
||||
source, default = Register(replace(line, text=rest), number).source()
|
||||
except PhoneSyntax as exc:
|
||||
self.d.warn(f"Phone register: {exc}", self.where(line))
|
||||
return
|
||||
if default is not None: # the group chat object, created with the other defaults of a new game
|
||||
created = self.expr(default[1], line)
|
||||
if created is not None:
|
||||
self.d.defaults.append([default[0], created])
|
||||
self.d.init_code.extend(self.python(source, line))
|
||||
# ...and filled right after it: a default whose value is a call of the function just defined
|
||||
self.d.defaults.append([f"__phone_register_{number}_done",
|
||||
{"k": "call", "f": {"k": "name", "id": f"__phone_register_{number}"}, "a": [], "kw": {}}])
|
||||
|
||||
@staticmethod
|
||||
def fold_live2d(children: list[Line]) -> list[Line]:
|
||||
"""`if renpy.has_live2d() and ...:` is false in the player (it has no Live2D), so only its `else` counts."""
|
||||
@@ -502,14 +565,31 @@ class RpyParser:
|
||||
c = self.cursor(line)
|
||||
c.keyword("python")
|
||||
if c.keyword("early"):
|
||||
self.init_python(line.block_source(), line)
|
||||
self.init_python(line.block_source(), line, self.store_name(c))
|
||||
return []
|
||||
if c.keyword("in"): # `python in some_store:` at run time: runs in that store
|
||||
store = c.dotted()
|
||||
return [{"op": "exec", "code": [{"op": "in_store", "ns": store, "body": self.python(line.block_source(), line)}]}]
|
||||
return self.python_nodes(self.python(line.block_source(), line), line)
|
||||
|
||||
def st_rpy(self, line: Line) -> list[Node]:
|
||||
return []
|
||||
|
||||
def init_python(self, source: str, line: Line) -> None:
|
||||
@staticmethod
|
||||
def store_name(c: Cursor) -> str | None:
|
||||
"""The `in phone.discussion` of `init python in phone.discussion:`."""
|
||||
return (c.dotted() or None) if c.keyword("in") else None
|
||||
|
||||
def init_python(self, source: str, line: Line, store: str | None = None) -> None:
|
||||
start = len(self.d.init_code)
|
||||
self.init_python_body(source, line)
|
||||
if store: # the names this block defines belong to a named store
|
||||
body = self.d.init_code[start:]
|
||||
del self.d.init_code[start:]
|
||||
if body:
|
||||
self.d.init_code.append({"op": "in_store", "ns": store, "body": body})
|
||||
|
||||
def init_python_body(self, source: str, line: Line) -> None:
|
||||
try:
|
||||
tree = _parse_py(source)
|
||||
except SyntaxError as e:
|
||||
@@ -535,6 +615,9 @@ class RpyParser:
|
||||
and ast.unparse(node.value.func) == "renpy.music.register_channel"):
|
||||
self.register_channel(node.value)
|
||||
continue
|
||||
if (isinstance(node, ast.Assign) and len(node.targets) == 1
|
||||
and ast.unparse(node.targets[0]).startswith("config.special_namespaces")):
|
||||
continue # lets `define phone.config.x = ...` name a store; named stores are plain objects here
|
||||
if isinstance(node, ast.Assign) and len(node.targets) == 1:
|
||||
tgt = node.targets[0]
|
||||
name = _assign_name(tgt)
|
||||
@@ -822,7 +905,7 @@ class RpyParser:
|
||||
layers.append({"pos": list(pos), "img": sub})
|
||||
return {"composite": layers, "size": list(size) if isinstance(size, (list, tuple))
|
||||
else None}
|
||||
if fname == "Animation": # pictures that take turns, named by values known only while playing
|
||||
if fname in ("Animation", "SnowBlossom"): # pictures that take turns / fall, built from values known while playing
|
||||
self.d.warn("Изображение задано выражением", self.where(line), blocking=False)
|
||||
return {"expr": e}
|
||||
# wrappers: Transform("x.png", zoom=.5), im.Scale(...), im.Flip(...), etc.
|
||||
@@ -1159,6 +1242,7 @@ class RpyParser:
|
||||
def st_transform(self, line: Line) -> list[Node]:
|
||||
c = self.cursor(line)
|
||||
c.keyword("transform")
|
||||
c.number() # `transform -150 _fits(size):`: an init priority
|
||||
name = c.word()
|
||||
if name:
|
||||
issues: list[tuple[str, str]] = []
|
||||
@@ -1178,7 +1262,7 @@ class RpyParser:
|
||||
return []
|
||||
|
||||
def transform_params(self, line: Line) -> list[dict[str, Any]]:
|
||||
match = re.fullmatch(r"transform\s+[A-Za-z_]\w*\s*\((.*)\)\s*:?", line.text.strip(), re.S)
|
||||
match = re.fullmatch(r"transform\s+(?:-?\d+\s+)?[A-Za-z_]\w*\s*\((.*)\)\s*:?", line.text.strip(), re.S)
|
||||
if not match:
|
||||
return []
|
||||
try:
|
||||
@@ -1278,7 +1362,7 @@ class RpyParser:
|
||||
self.d.warn("Не разобран фрагмент show/scene", f"{self.where(line)}: {c.rest()}")
|
||||
break
|
||||
|
||||
def transform_ref(self, source: str, line: Line) -> dict[str, Any]:
|
||||
def transform_ref(self, source: str, line: Line, dynamic: bool = False) -> dict[str, Any]:
|
||||
source = source.strip()
|
||||
if source in POSITIONS:
|
||||
return {"pos": source}
|
||||
@@ -1318,6 +1402,10 @@ class RpyParser:
|
||||
if e and e["k"] == "name" and e["id"] not in self.d.transforms:
|
||||
# transform defined later in another file; the player resolves it by name
|
||||
return {"name": e["id"]}
|
||||
if dynamic and e and e["k"] == "call" and e["f"].get("k") in ("name", "attr"):
|
||||
# in a screen, a Python function that makes a transform (`CurriedRoundedCorners(radius=...)`): evaluated when it is shown
|
||||
self.d.warn("Трансформация вычисляется при показе", f"{self.where(line)}: {source}", blocking=False)
|
||||
return {"dyn": e}
|
||||
self.d.warn("Неизвестная трансформация", f"{self.where(line)}: {source}")
|
||||
return {"name": source}
|
||||
|
||||
@@ -1344,7 +1432,7 @@ class RpyParser:
|
||||
else:
|
||||
collect.append((message, self.where(line)))
|
||||
|
||||
return ScreenParser(warn, lambda source, line: self.transform_ref(source, line),
|
||||
return ScreenParser(warn, lambda source, line: self.transform_ref(source, line, dynamic=True),
|
||||
lambda lines: self.atl_spec(lines, collect.append if collect is not None else None, set()))
|
||||
|
||||
def st_screen(self, line: Line) -> list[Node]:
|
||||
|
||||
Reference in new issue
Block a user