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, show,
voice, voice,
with, with,
stop,
pause,
hide,
}; };
struct jump { struct jump {
@ -70,6 +73,18 @@ namespace language {
struct with { struct with {
}; };
struct stop {
uint32_t channelIndex;
};
struct pause {
float duration;
};
struct hide {
uint32_t imageIndex;
};
struct statement { struct statement {
enum type type; enum type type;
union { union {

View File

@ -45,6 +45,11 @@ class TT(Enum):
RETURN = auto() RETURN = auto()
INIT = auto() INIT = auto()
FADEOUT = auto() FADEOUT = auto()
TRANSFORM = auto()
STOP = auto()
NOLOOP = auto()
PAUSE = auto()
HIDE = auto()
keywords = { keywords = {
b"play": TT.PLAY, b"play": TT.PLAY,
@ -61,6 +66,11 @@ keywords = {
b"return": TT.RETURN, b"return": TT.RETURN,
b"init": TT.INIT, b"init": TT.INIT,
b"fadeout": TT.FADEOUT, 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 @dataclass

View File

@ -56,6 +56,14 @@ class Play:
channel: lex.Token channel: lex.Token
path: lex.Token path: lex.Token
fadeout: lex.Token fadeout: lex.Token
noloop: bool
__repr__ = lexeme_repr
@dataclass
class Stop:
channel: lex.Token
fadeout: lex.Token
__repr__ = lexeme_repr __repr__ = lexeme_repr
@ -88,6 +96,7 @@ class Voice:
class Show: class Show:
what: lex.Token what: lex.Token
transform: lex.Token transform: lex.Token
properties: list[tuple[lex.Token, lex.Token]]
__repr__ = lexeme_repr __repr__ = lexeme_repr
@ -102,6 +111,18 @@ class Jump:
__repr__ = lexeme_repr __repr__ = lexeme_repr
@dataclass
class Pause:
duration: lex.Token
__repr__ = lexeme_repr
@dataclass
class Hide:
what: lex.Token
__repr__ = lexeme_repr
@dataclass @dataclass
class Return: class Return:
pass pass
@ -242,11 +263,16 @@ def parse_play(tokens, index):
if fadeout.type != TT.NUMBER: if fadeout.type != TT.NUMBER:
raise ParseException("expected number", fadeout) raise ParseException("expected number", fadeout)
index += 2 index += 2
noloop = False
if token.type == TT.NOLOOP:
noloop = True
index += 1
play = Play( play = Play(
channel = channel, channel = channel,
path = path, path = path,
fadeout = fadeout fadeout = fadeout,
noloop = noloop,
) )
return index, play return index, play
@ -290,6 +316,12 @@ def parse_voice(tokens, index):
return index + 1, voice return index + 1, voice
def parse_show(tokens, index): 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) index, what = parse_lhs(tokens, index)
at = tokens[index + 0] at = tokens[index + 0]
@ -300,11 +332,36 @@ def parse_show(tokens, index):
if transform.type != TT.IDENTIFIER: if transform.type != TT.IDENTIFIER:
raise ParseException("expected identifier", transform) 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( show = Show(
what = what, what = what,
transform = transform transform = transform,
properties = properties
) )
return index + 2, show return index, show
def parse_menu(tokens, index): def parse_menu(tokens, index):
menu = tokens[index + 0] menu = tokens[index + 0]
@ -369,7 +426,7 @@ def parse_init(tokens, index):
colon = tokens[index + 1] colon = tokens[index + 1]
if colon.type != TT.COLON: if colon.type != TT.COLON:
raise ParseException("expected identifier", colon) raise ParseException("expected colon", colon)
index += 2 index += 2
@ -388,6 +445,74 @@ def parse_init(tokens, index):
return index, None 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): def parse_one(tokens, index):
token = tokens[index] token = tokens[index]
if token.type == TT.NEWLINE: if token.type == TT.NEWLINE:
@ -421,7 +546,7 @@ def parse_one(tokens, index):
index, ast = parse_voice(tokens, index + 1) index, ast = parse_voice(tokens, index + 1)
return index, ast return index, ast
elif token.type == TT.SHOW: elif token.type == TT.SHOW:
index, ast = parse_show(tokens, index + 1) index, ast = parse_show(tokens, index)
return index, ast return index, ast
elif token.type == TT.MENU: elif token.type == TT.MENU:
index, ast = parse_menu(tokens, index) index, ast = parse_menu(tokens, index)
@ -434,6 +559,18 @@ def parse_one(tokens, index):
elif token.type == TT.INIT: elif token.type == TT.INIT:
index, ast = parse_init(tokens, index) index, ast = parse_init(tokens, index)
return index, ast 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: else:
raise ParseException("unexpected token", token) raise ParseException("unexpected token", token)

View File

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