`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>
193 lines
8.4 KiB
Python
193 lines
8.4 KiB
Python
"""The ``init phone register`` statement of the Phone library (a chat app games put on a phone).
|
|
|
|
In Ren'Py the library registers the statement with its own parser, which runs while the game's scripts are read;
|
|
the converter cannot run it, so the statement is read here and turned into the Python the library would run:
|
|
|
|
init phone register:
|
|
define "[nova]":
|
|
add "mc" add "nova"
|
|
icon phone.asset("nova.png")
|
|
as novaTexts key "novaTexts"
|
|
label "Previous messages cannot be loaded."
|
|
time year 2023 month 12 day 25 hour 7 minute 7
|
|
"nova" "Morning!"
|
|
|
|
becomes ``default novaTexts = phone.group_chat.GroupChat(...)`` plus a function that adds the characters and calls
|
|
``phone.discussion.register_message/_label/_date/_image/_audio`` for each line. The function runs with the other
|
|
defaults of a new game, after the group chat exists. Only what the library's own parser accepts is read; anything
|
|
else raises ``PhoneSyntax`` and the statement is reported as unsupported.
|
|
"""
|
|
import re
|
|
|
|
from app.converter.rpy_lexer import Line
|
|
|
|
STRING = r"""(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')"""
|
|
TIME_PARTS = ("month", "day", "year", "hour", "minute", "second")
|
|
|
|
|
|
class PhoneSyntax(Exception):
|
|
pass
|
|
|
|
|
|
def _split_expression(text: str) -> tuple[str, str]:
|
|
"""The first simple expression of `text` (a string, or a name with brackets) and what follows it."""
|
|
text = text.lstrip()
|
|
if (m := re.match(STRING, text)):
|
|
return m[0], text[m.end():].lstrip()
|
|
depth, i = 0, 0
|
|
while i < len(text):
|
|
ch = text[i]
|
|
if ch in "([{":
|
|
depth += 1
|
|
elif ch in ")]}":
|
|
depth -= 1
|
|
elif ch.isspace() and depth == 0:
|
|
break
|
|
i += 1
|
|
if not text[:i]:
|
|
raise PhoneSyntax("ожидалось выражение")
|
|
return text[:i], text[i:].lstrip()
|
|
|
|
|
|
class Register:
|
|
"""One `init phone register` block turned into Python source."""
|
|
|
|
def __init__(self, line: Line, number: int):
|
|
self.line = line
|
|
self.number = number
|
|
header = line.text.strip()
|
|
match = re.fullmatch(r"phone\s+register\s*(.*?):", header, re.S)
|
|
if not match:
|
|
raise PhoneSyntax("ожидалось `phone register`")
|
|
self.target = match[1].strip() or None # an existing group chat, or None when the block defines one
|
|
self.default: tuple[str, str] | None = None # (variable, expression)
|
|
self.body: list[str] = []
|
|
|
|
def source(self) -> tuple[str, tuple[str, str] | None]:
|
|
children = list(self.line.block)
|
|
lines: list[str] = []
|
|
if self.target is None:
|
|
if not children or not children[0].text.strip().startswith("define"):
|
|
raise PhoneSyntax("ожидалось `define`")
|
|
define = children.pop(0)
|
|
lines.extend(self.define(define))
|
|
else:
|
|
lines.append(f"gc = phone.group_chat.group_chat({self.target})")
|
|
if not children and self.target is not None:
|
|
raise PhoneSyntax("ожидался хотя бы один оператор")
|
|
lines.extend(self.statements(children, 1))
|
|
function = f"__phone_register_{self.number}"
|
|
text = f"def {function}():\n" + "\n".join(" " + item for item in lines) + "\n pass\n"
|
|
return text, self.default
|
|
|
|
# -- the group chat -------------------------------------------------------------------
|
|
|
|
def define(self, line: Line) -> list[str]:
|
|
match = re.fullmatch(rf"define\s+({STRING})\s*:", line.text.strip(), re.S)
|
|
if not match:
|
|
raise PhoneSyntax("ожидалось `define \"имя\":`")
|
|
name = match[1]
|
|
icon = key = variable = None
|
|
characters: list[str] = []
|
|
transient = False
|
|
for child in line.block:
|
|
rest = child.text.strip()
|
|
while rest:
|
|
word, _, tail = rest.partition(" ")
|
|
rest = tail.lstrip()
|
|
if word == "key":
|
|
key, rest = _split_expression(rest)
|
|
elif word == "icon":
|
|
icon, rest = _split_expression(rest)
|
|
elif word == "add":
|
|
character, rest = _split_expression(rest)
|
|
characters.append(character)
|
|
elif word == "transient":
|
|
transient = True
|
|
elif word == "as":
|
|
variable, rest = _split_expression(rest)
|
|
else:
|
|
raise PhoneSyntax(f"неизвестное свойство {word}")
|
|
if key is None:
|
|
raise PhoneSyntax("ожидалось свойство key")
|
|
icon = icon or 'phone.asset("default_icon.png")'
|
|
create = f"phone.group_chat.GroupChat({name}, {icon}, {key}, transient={transient})"
|
|
out = []
|
|
if variable is not None:
|
|
self.default = (variable, create)
|
|
out.append(f"gc = {variable}")
|
|
else:
|
|
out.append(f"gc = {create}")
|
|
out.extend(f"gc.add_character(phone.character.character({c}))" for c in characters)
|
|
return out
|
|
|
|
# -- the lines ------------------------------------------------------------------------
|
|
|
|
def statements(self, lines: list[Line], depth: int) -> list[str]:
|
|
out: list[str] = []
|
|
i = 0
|
|
while i < len(lines):
|
|
line = lines[i]
|
|
text = line.text.strip()
|
|
i += 1
|
|
if text.startswith("if ") or text.startswith("if("):
|
|
condition = text[2:].strip().removesuffix(":").strip()
|
|
out.append(f"if {condition}:")
|
|
out.extend(" " + item for item in self.statements(line.block, depth + 1) or ["pass"])
|
|
while i < len(lines) and re.match(r"(elif\b|else\s*:)", lines[i].text.strip()):
|
|
branch = lines[i].text.strip().removesuffix(":").strip()
|
|
out.append(f"{branch}:")
|
|
out.extend(" " + item for item in self.statements(lines[i].block, depth + 1) or ["pass"])
|
|
i += 1
|
|
continue
|
|
out.extend(self.one(line, text))
|
|
return out
|
|
|
|
def one(self, line: Line, text: str) -> list[str]:
|
|
if text == "pass":
|
|
return []
|
|
if text.startswith("$"):
|
|
return [text[1:].strip()]
|
|
if re.fullmatch(r"python\s*:", text):
|
|
return [row for row in line.block_source().splitlines() if row.strip()]
|
|
if (m := re.match(r"label\s+(" + STRING + r")\s*$", text)):
|
|
return [f"phone.discussion.register_label(gc, {m[1]})"]
|
|
if text.startswith("time ") or text == "time":
|
|
return [self.date(text[4:].strip())]
|
|
if (m := re.match(r"image\s+(.*)", text)):
|
|
sender, rest = _split_expression(m[1])
|
|
image, rest = _split_expression(rest)
|
|
if rest:
|
|
raise PhoneSyntax("лишнее после image")
|
|
return [f"phone.discussion.register_image(gc, {sender}, {image})"]
|
|
if (m := re.match(r"audio\s+(.*)", text)):
|
|
sender, rest = _split_expression(m[1])
|
|
audio, rest = _split_expression(rest)
|
|
if rest:
|
|
raise PhoneSyntax("лишнее после audio")
|
|
return [f"phone.discussion.register_audio(gc, {sender}, {audio})"]
|
|
sender, rest = _split_expression(text)
|
|
message, rest = _split_expression(rest)
|
|
if rest or not re.fullmatch(STRING, message):
|
|
raise PhoneSyntax("ожидалось: отправитель \"сообщение\"")
|
|
return [f"phone.discussion.register_message(gc, {sender}, {message})"]
|
|
|
|
def date(self, text: str) -> str:
|
|
values = {part: "None" for part in TIME_PARTS}
|
|
auto = "False"
|
|
seen: set[str] = set()
|
|
rest = text
|
|
while rest:
|
|
word, _, tail = rest.partition(" ")
|
|
if word not in values and word != "auto":
|
|
raise PhoneSyntax(f"`{word}` не свойство time")
|
|
if word in seen:
|
|
raise PhoneSyntax(f"`{word}` указано дважды")
|
|
if word == "auto":
|
|
raise PhoneSyntax("`auto` нельзя в init phone register")
|
|
value, rest = _split_expression(tail)
|
|
values[word] = value
|
|
seen.add(word)
|
|
arguments = ", ".join(f"{k}={v}" for k, v in values.items())
|
|
return f"phone.discussion.register_date(gc, auto={auto}, {arguments})"
|