WTS/game/scripts/utility/lint.rpy
2022-05-17 00:48:22 +01:00

273 lines
11 KiB
Plaintext

init:
if config.developer:
# Dummy code used to stop lint from reporting false errors.
image snape_main = Null()
image genie_main = Null()
image tonks_main = Null()
image hermione_main = Null()
image cho_main = Null()
image luna_main = Null()
image astoria_main = Null()
image susan_main = Null()
image hooch_main = Null()
image hooch = Null()
init -1 python:
if config.developer:
def lint_characters():
def strip(s):
# We need a custom strip implementation because we cannot tell
# if the raw argument isn't encapsulated in double, or triple quotes
if s.startswith(('"', "'")) and s.endswith(('"', "'")):
return s[1:-1]
return s
renpy.execute_default_statement(False)
# Add images to linting list to avoid undefined errors
for i in CHARACTERS:
prefix = "{}_main".format(i)
renpy.lint.image_prefixes[prefix] = True
nodes = [i for i in renpy.game.script.all_stmts if isinstance(i, (renpy.ast.Say, renpy.ast.Menu))]
nodes.sort(key=lambda x: (x.filename, x.linenumber))
SELF_CLOSING_TAGS = re.compile(r'(\{\{)|(\{(p|w|nw|fast|done|image|space)(?:\=([^}]*))?\})', re.S)
EXPRESSIONS = ["mouth", "eyes", "eyebrows", "pupils", "cheeks", "tears"]
POSITIONS = ["xpos", "ypos", "pos"]
OTHER = ["flip", "trans", "animation", "hair", "emote"]
KEYS = EXPRESSIONS + POSITIONS + OTHER
INTERPUNCT = (".", "!", "?", "--", "*", ")", "}")
RE_SPACES = re.compile(r"\s+\s+")
RE_COMMA = re.compile(r"\w+, \w+ and")
RE_BRACKET = re.compile(r"\([^)]*$")
RE_REPEATED = re.compile(r"\b(\w+)\b \b\1\b")
RE_QUOTES = re.compile(r"[.,]\\\"")
RE_ARTICLES = re.compile(r" a [aeiou][^uni|eu|ur|usa]")
for node in nodes:
if isinstance(node, renpy.ast.Say):
who = node.who
what = [node.what]
file = node.filename
line = node.linenumber
args = node.arguments
code = node.get_code()
type = "say"
elif isinstance(node, renpy.ast.Menu):
who = None
what = [x[0] for x in node.items]
file = node.filename
line = node.linenumber
args = None
code = None
type = "menu"
has_failed = False
has_xpos = False
has_ypos = False
uses_ypos_head = False
if args:
kwargs = dict.fromkeys(KEYS)
args = args.arguments
for i, arg in enumerate(args):
key, val = arg
if key is None:
kwargs[KEYS[i]] = val
else:
kwargs[key] = val
for key, val in kwargs.iteritems():
# Validate facial expressions
if key in EXPRESSIONS and who in SAYERS.iterkeys():
if val is None:
continue
if not val.startswith(("\"", "'")) and not val.endswith(("\"", "'")): # Ignore variables
continue
# Node argument values are (fucking) raw
val = strip(val)
msg = "'{}'".format(key)
fn = "characters/{}/face/{}/{}.webp".format(SAYERS.get(who), key, val)
if not has_failed:
# Avoid repeating node destination
renpy.lint.report_node = node
has_failed = True
renpy.lint.check_file(msg, fn)
# Validate positional arguments
if key in POSITIONS:
if val is None:
continue
# Node argument values are (fucking) raw
val = strip(val)
# Skip integers
try:
int(val)
continue
except:
pass
if key == "xpos":
has_xpos = True
val = sprite_pos.get("x").get(val, val)
elif key == "ypos":
has_ypos = True
if val == "head":
uses_ypos_head = True
val = sprite_pos.get("y").get(val, val)
if not isinstance(val, int):
if not has_failed:
# Avoid repeating node destination
renpy.lint.report_node = node
has_failed = True
msg = "'{}' requires an integer, or a pre-defined named position, not '{}'".format(key, val)
renpy.lint.report(msg)
# This would require fixing hundreds of calls. Might postpone it...
# if uses_ypos_head and not has_xpos:
# if not has_failed:
# # Avoid repeating node destination
# renpy.lint.report_node = node
# has_failed = True
# msg = "'ypos' uses pre-defined 'head' position, but is lacking 'xpos'"
# renpy.lint.report(msg)
if key in OTHER:
if val is None:
continue
# Node argument values are (fucking) raw
val = strip(val)
if key == "emote":
msg = "'{}'".format(key)
fn = "characters/{}/emote/{}.webp".format(SAYERS.get(who), val)
if not has_failed:
# Avoid repeating node destination
renpy.lint.report_node = node
has_failed = True
renpy.lint.check_file(msg, fn)
for string in what:
if not string:
continue
if string.startswith("[") and string.endswith("]"): # Ignore string interpolation
continue
# Check for multiple spaces inside 'what' string
match = re.search(RE_SPACES, string.strip())
if match:
if not has_failed:
# Avoid repeating node destination
renpy.lint.report_node = node
has_failed = True
msg = type + " string contains multiple spaces"
renpy.lint.report(msg)
# Check for the (lack of) oxford comma inside 'what' string
match = re.search(RE_COMMA, string.strip())
if match:
if not has_failed:
# Avoid repeating node destination
renpy.lint.report_node = node
has_failed = True
msg = type + " string is missing oxford comma(s)"
renpy.lint.report(msg)
# Check for missing brackets inside 'what' string
match = re.search(RE_BRACKET, string.strip())
if match:
if not has_failed:
# Avoid repeating node destination
renpy.lint.report_node = node
has_failed = True
msg = type + " string is missing a closing bracket"
renpy.lint.report(msg)
# Check for repeated words words inside 'what' string
match = re.search(RE_REPEATED, string.strip())
if match:
if not has_failed:
# Avoid repeating node destination
renpy.lint.report_node = node
has_failed = True
msg = type + " string contains repeated words"
renpy.lint.report(msg)
# Check for punctuation inside quotes, inside 'what' string
match = re.search(RE_QUOTES, string.strip())
if match:
if not has_failed:
# Avoid repeating node destination
renpy.lint.report_node = node
has_failed = True
msg = type + " string contains non-british punctuation for quotes"
renpy.lint.report(msg)
# Check for wrong articles inside 'what' string
match = re.search(RE_ARTICLES, string.strip())
if match:
if not has_failed:
# Avoid repeating node destination
renpy.lint.report_node = node
has_failed = True
msg = type + " string contains wrong articles"
renpy.lint.report(msg)
if type == "menu" and string.startswith(("-", "\"")) and string.endswith(("-", "\"")):
continue
# Check punctuation errors
if not string.endswith(INTERPUNCT):
if not has_failed:
# Avoid repeating node destination
renpy.lint.report_node = node
has_failed = True
msg = type + " string punctuation is not adhering to the set text styling"
renpy.lint.report(msg)
config.lint_hooks.append(lint_characters)