renpy-parser: add support for May 24 syntax

This commit is contained in:
Zack Buhman 2026-05-25 16:19:02 -05:00
parent e3ffcce26f
commit 9112e74b8b
4 changed files with 208 additions and 11 deletions

View File

@ -30,6 +30,9 @@ namespace language {
show,
voice,
with,
stop,
pause,
hide,
};
struct jump {
@ -70,6 +73,18 @@ namespace language {
struct with {
};
struct stop {
uint32_t channelIndex;
};
struct pause {
float duration;
};
struct hide {
uint32_t imageIndex;
};
struct statement {
enum type type;
union {

View File

@ -45,6 +45,11 @@ class TT(Enum):
RETURN = auto()
INIT = auto()
FADEOUT = auto()
TRANSFORM = auto()
STOP = auto()
NOLOOP = auto()
PAUSE = auto()
HIDE = auto()
keywords = {
b"play": TT.PLAY,
@ -61,6 +66,11 @@ keywords = {
b"return": TT.RETURN,
b"init": TT.INIT,
b"fadeout": TT.FADEOUT,
b"transform": TT.TRANSFORM,
b"stop": TT.STOP,
b"noloop": TT.NOLOOP,
b"pause": TT.PAUSE,
b"hide": TT.HIDE,
}
@dataclass

View File

@ -56,6 +56,14 @@ class Play:
channel: lex.Token
path: lex.Token
fadeout: lex.Token
noloop: bool
__repr__ = lexeme_repr
@dataclass
class Stop:
channel: lex.Token
fadeout: lex.Token
__repr__ = lexeme_repr
@ -88,6 +96,7 @@ class Voice:
class Show:
what: lex.Token
transform: lex.Token
properties: list[tuple[lex.Token, lex.Token]]
__repr__ = lexeme_repr
@ -102,6 +111,18 @@ class Jump:
__repr__ = lexeme_repr
@dataclass
class Pause:
duration: lex.Token
__repr__ = lexeme_repr
@dataclass
class Hide:
what: lex.Token
__repr__ = lexeme_repr
@dataclass
class Return:
pass
@ -242,11 +263,16 @@ def parse_play(tokens, index):
if fadeout.type != TT.NUMBER:
raise ParseException("expected number", fadeout)
index += 2
noloop = False
if token.type == TT.NOLOOP:
noloop = True
index += 1
play = Play(
channel = channel,
path = path,
fadeout = fadeout
fadeout = fadeout,
noloop = noloop,
)
return index, play
@ -290,6 +316,12 @@ def parse_voice(tokens, index):
return index + 1, voice
def parse_show(tokens, index):
show = tokens[index + 0]
if show.type != TT.SHOW:
raise ParseException("expected show", show)
index += 1
index, what = parse_lhs(tokens, index)
at = tokens[index + 0]
@ -300,11 +332,36 @@ def parse_show(tokens, index):
if transform.type != TT.IDENTIFIER:
raise ParseException("expected identifier", transform)
index += 2
properties = []
if tokens[index + 0].type == TT.COLON:
index += 1
while index < len(tokens):
token = tokens[index + 0]
if token.type == TT.NEWLINE:
index += 1
continue
if token.position.column <= show.position.column:
break
if token.type != TT.IDENTIFIER:
raise ParseException("expected identifier")
number = tokens[index + 1]
if number.type != TT.NUMBER:
raise ParseException("expected number")
properties.append((token, number))
index += 2
show = Show(
what = what,
transform = transform
transform = transform,
properties = properties
)
return index + 2, show
return index, show
def parse_menu(tokens, index):
menu = tokens[index + 0]
@ -369,7 +426,7 @@ def parse_init(tokens, index):
colon = tokens[index + 1]
if colon.type != TT.COLON:
raise ParseException("expected identifier", colon)
raise ParseException("expected colon", colon)
index += 2
@ -388,6 +445,74 @@ def parse_init(tokens, index):
return index, None
def parse_transform(tokens, index):
transform = tokens[index + 0]
if transform.type != TT.TRANSFORM:
raise ParseException("expected transform", init)
identifier = tokens[index + 1]
if identifier.type != TT.IDENTIFIER:
raise ParseException("expected identifier", identifier)
colon = tokens[index + 2]
if colon.type != TT.COLON:
raise ParseException("expected colon", colon)
index += 3
# skip all tokens inside block
while index < len(tokens):
token = tokens[index]
if token.type == TT.NEWLINE:
index += 1
continue
if token.position.column < transform.position.column:
raise ParseException("invalid init block dedent", token)
if token.position.column == transform.position.column:
break
index += 1
return index, None
def parse_stop(tokens, index):
channel = tokens[index + 0]
if channel.type != TT.IDENTIFIER:
raise ParseException("expected identifier", channel)
index += 1
token = tokens[index]
fadeout = None
if token.type == TT.FADEOUT:
fadeout = tokens[index + 1]
if fadeout.type != TT.NUMBER:
raise ParseException("expected number", fadeout)
index += 2
stop = Stop(
channel = channel,
fadeout = fadeout
)
return index, stop
def parse_pause(tokens, index):
duration = tokens[index + 0]
if duration.type != TT.NUMBER:
raise ParseException("expected number", duration)
pause = Pause(
duration = duration
)
return index + 1, pause
def parse_hide(tokens, index):
index, what = parse_lhs(tokens, index)
hide = Hide(
what = what
)
return index + 1, hide
def parse_one(tokens, index):
token = tokens[index]
if token.type == TT.NEWLINE:
@ -421,7 +546,7 @@ def parse_one(tokens, index):
index, ast = parse_voice(tokens, index + 1)
return index, ast
elif token.type == TT.SHOW:
index, ast = parse_show(tokens, index + 1)
index, ast = parse_show(tokens, index)
return index, ast
elif token.type == TT.MENU:
index, ast = parse_menu(tokens, index)
@ -434,6 +559,18 @@ def parse_one(tokens, index):
elif token.type == TT.INIT:
index, ast = parse_init(tokens, index)
return index, ast
elif token.type == TT.TRANSFORM:
index, ast = parse_transform(tokens, index)
return index, ast
elif token.type == TT.STOP:
index, ast = parse_stop(tokens, index + 1)
return index, ast
elif token.type == TT.PAUSE:
index, ast = parse_pause(tokens, index + 1)
return index, ast
elif token.type == TT.HIDE:
index, ast = parse_hide(tokens, index + 1)
return index, ast
else:
raise ParseException("unexpected token", token)

View File

@ -43,6 +43,9 @@ simple_statement_types = {
parse.Show,
parse.Voice,
parse.With,
parse.Stop,
parse.Pause,
parse.Hide,
}
def pass1(state, ast):
@ -111,7 +114,7 @@ def pass2_statement(state, pc, statement):
if type(statement) is parse.Play:
comment = statement.path.lexeme.decode('utf-8')
audio_index = state.audio_lookup[statement.path.lexeme]
yield f"{{ .type = type::play, .play = {{ .audioIndex = {audio_index} }} }}, // {pc} {comment}"
yield f"{{ .type = type::play, .play = {{ .audioIndex = {audio_index}, /* FIXME channel */ }} }}, // {pc} {comment}"
elif type(statement) is parse.Scene:
key = lhs_key(statement.name)
image_index = state.images_lookup[key]
@ -152,6 +155,16 @@ def pass2_statement(state, pc, statement):
yield f"{{ .type = type::jump, .jump = {{ .statementIndex = {statement_index} }} }}, // {pc} {comment}"
elif type(statement) is parse.Return:
yield f"{{ .type = type::_return }}, // {pc}"
elif type(statement) is parse.Stop:
yield f"{{ .type = type::stop, .stop = {{ /* FIXME channel */ }} }}, // {pc}"
elif type(statement) is parse.Pause:
duration = statement.duration.lexeme
yield f"{{ .type = type::pause, .pause = {{ .duration = {duration} }} }}, // {pc}"
elif type(statement) is parse.Hide:
key = lhs_key(statement.what)
image_index = state.images_lookup[key]
comment = ".".join(k.decode('utf-8') for k in key)
yield f"{{ .type = type::hide, .hide = {{ .imageIndex = {image_index} }} }}, // {pc} {comment}"
else:
pass
assert False, (type(statement), statement)
@ -161,12 +174,14 @@ def pass2_statements(state):
for pc, statement in enumerate(state.statements):
yield from pass2_statement(state, pc, statement)
yield "};"
yield "constexpr int statements_length = (sizeof (statements)) / (sizeof (statements[0]));"
def pass2_strings(state):
yield "char const * const strings[] = {"
for string, i in sorted(state.string_lookup.items(), key=lambda kv: kv[1]):
yield f"\"{string.decode('utf-8')}\", // {i}"
yield "};"
yield "constexpr int strings_length = (sizeof (strings)) / (sizeof (strings[0]));"
def pass2_characters(state):
yield "const character characters[] = {"
@ -174,24 +189,42 @@ def pass2_characters(state):
character_name, = character.value.args
yield f"{{ .characterName = \"{character_name.lexeme.decode('utf-8')}\" }}, // {i}"
yield "};"
yield "constexpr int characters_length = (sizeof (characters)) / (sizeof (characters[0]));"
def pass2_audio(state):
yield "const audio audio[] = {"
for audio, i in sorted(state.audio_lookup.items(), key=lambda kv: kv[1]):
yield f"{{ .path = \"{audio.decode('utf-8')}\" }}, // {i}"
orig_path = audio.decode('utf-8')
path = orig_path
if path.endswith(".mp3"):
path = path.removesuffix(".mp3")
elif path.endswith(".ogg"):
path = path.removesuffix(".ogg")
else:
assert False, path
yield f"{{ .path = \"{path}.opus\" }}, // {i} {orig_path}"
yield "};"
yield "constexpr int audio_length = (sizeof (audio)) / (sizeof (audio[0]));"
def pass2_images(state):
yield "const image image[] = {"
yield "const image images[] = {"
for i, image in enumerate(state.images):
yield f"{{ .path = \"{image.path.lexeme.decode('utf-8')}\" }}, // {i}"
orig_path = image.path.lexeme.decode('utf-8')
path = orig_path
if path.endswith(".png"):
path = path.removesuffix(".png")
else:
assert False, path
yield f"{{ .path = \"{path}.dds\" }}, // {i} {orig_path}"
yield "};"
yield "constexpr int images_length = (sizeof (images)) / (sizeof (images[0]));"
def pass2_options(state):
yield "const option options[] = {"
for i, (lexeme, statement_index) in sorted(state.entries.items(), key=lambda kv: kv[0]):
yield f"{{ .string = \"{lexeme.decode('utf-8')}\", .statementIndex = {statement_index} }}, // {i}"
yield "};"
yield "constexpr int options_length = (sizeof (options)) / (sizeof (options[0]));"
def pass2(state):
yield "#include \"statement.h\""
@ -224,9 +257,11 @@ def main():
global_identifiers = set(),
)
try:
ast_list = list(parse.parse_all(tokens))
ast_list = []
for ast in parse.parse_all(tokens):
ast_list.append(ast)
except parse.ParseException as e:
print(e, e.token)
print(e, e.token, file=sys.stderr)
raise
for t in ast_list: