Apparently if you create a dict with pre-populated data, pyright infers the dict's types for type checking purposes, and this can be wrong here where my data dicts are massive sprawling affairs of dynamic info. The right thing to do is probably turn this into a class, but for now I'll just split the dicts pyright complains about up differently.
301 lines
11 KiB
Python
301 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
|
|
__version__ = "0.2"
|
|
__author__ = "Trysdyn Black"
|
|
|
|
import json
|
|
import sys
|
|
|
|
|
|
def parse_MONSTERS(data):
|
|
result = {}
|
|
for m_text in data.split("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"):
|
|
info = {}
|
|
info["stats"] = {}
|
|
info["spells"] = {}
|
|
name = "NULL"
|
|
for line in m_text.split("\n"):
|
|
# Name and level
|
|
if "(Level " in line:
|
|
name = line.split(" (")[0]
|
|
info["stats"]["level"] = int(line.split("Level ")[1][:-1])
|
|
# Stat chart rows
|
|
elif line.startswith("|"):
|
|
for stat in line[1:-1].split("|"):
|
|
if ":" in stat:
|
|
stat_name, stat_value = stat.split(":")
|
|
stat_name = stat_name.replace(".", "").strip().lower()
|
|
info["stats"][stat_name] = int(stat_value)
|
|
# Nullifies AND weaks, split by a ;
|
|
elif line.startswith("NULLIFY:"):
|
|
# If no weaknesses, WEAK section just doesn't appear, fudge it
|
|
if "WEAK:" in line:
|
|
null_text, weak_text = line.split(";")
|
|
else:
|
|
null_text = line
|
|
weak_text = "WEAK: "
|
|
info["nullifies"] = null_text.split(": ")[1].split(", ")
|
|
info["weak"] = weak_text.split(": ")[1].split(", ")
|
|
elif line.startswith("IMMUNE:"):
|
|
if len(line) >= 9:
|
|
info["immune"] = line.split(": ")[1].split(", ")
|
|
else:
|
|
info["immune"] = []
|
|
elif line.startswith("AUTO:"):
|
|
if len(line) >= 7:
|
|
info["auto"] = line.split(": ")[1].split(", ")
|
|
else:
|
|
info["auto"] = []
|
|
# Specials are name=>desc as k:v
|
|
# I *think* you can only have one special...
|
|
elif line.startswith("SPECIAL"):
|
|
content = line.split(" ", 1)[1]
|
|
if len(content) > 1:
|
|
special_name = content.split('"')[1]
|
|
special_desc = content.split(": ")[1]
|
|
info["special"] = {special_name: special_desc}
|
|
else:
|
|
info["special"] = {}
|
|
elif line.startswith("SKILLS:"):
|
|
if len(line) >= 9:
|
|
info["skills"] = line.split(": ")[1].split(", ")
|
|
info["skills"] = []
|
|
elif line.startswith("STEAL:"):
|
|
if len(line) >= 8:
|
|
info["steal"] = line.split(": ")[1].split(", ")
|
|
else:
|
|
info["steal"] = []
|
|
elif line.startswith("DROPS:"):
|
|
if len(line) >= 8:
|
|
info["drops"] = line.split(": ")[1].split(", ")
|
|
else:
|
|
info["drops"] = []
|
|
elif line.startswith("LOCATION:"):
|
|
if len(line) >= 11:
|
|
info["location"] = line.split(": ", 1)[1]
|
|
else:
|
|
info["location"] = None
|
|
|
|
if name != "NULL":
|
|
result[name] = info
|
|
|
|
return result
|
|
|
|
|
|
def parse_REMONSTERATE(data):
|
|
# BCCE only. Remapping info if you use BCCE to also remonsterate
|
|
result = {}
|
|
for line in data.split("\n"):
|
|
if not line or line.startswith("-----"):
|
|
continue
|
|
name = line.split("(")[0].strip()
|
|
originally = line.split("(", 1)[1].split(")")[0].strip()
|
|
sprite = line.split("->")[1].strip().strip(".")
|
|
|
|
result[name] = {"originally": originally, "sprite": sprite}
|
|
|
|
return result
|
|
|
|
|
|
def parse_CHARACTERS(data):
|
|
replacements = {"Looks like": "looks", "World of Ruin location": "wor_location", "Notable equipment": "equipment"}
|
|
|
|
result = {}
|
|
|
|
for c_data in data.split("\n\n")[1:-1]:
|
|
info = {"stats": {}, "spells": {}, "natural_magic": False}
|
|
name = "NULL"
|
|
|
|
for line in c_data.split("\n"):
|
|
# Name
|
|
if line[0:2].isdigit():
|
|
name = line[4:]
|
|
|
|
# Stat chart rows
|
|
# BCEX Version Only
|
|
elif line.startswith("|"):
|
|
for stat in line[1:-1].split("|"):
|
|
if ":" in stat:
|
|
stat_name, stat_value = stat.split(":")
|
|
stat_name = stat_name.replace(".", "").strip().lower()
|
|
info["stats"][stat_name] = int(stat_value)
|
|
|
|
# Spell learnset rows
|
|
elif line.startswith(" LV"):
|
|
spell_level, spell_name = line.split("-", 1)
|
|
info["spells"][spell_name.strip()] = int(spell_level.strip().split(" ")[1])
|
|
|
|
# Special k=v strings with comma-delimited lists
|
|
elif line.startswith("Commands:"):
|
|
info["commands"] = [command.strip() for command in line.split(":")[1].split(",")]
|
|
|
|
elif line.startswith("Notable"):
|
|
info["equipment"] = [eq.strip() for eq in line.split(":")[1].split(",")]
|
|
|
|
# Special bare strings
|
|
elif line.startswith("Has natural"):
|
|
info["natural_magic"] = True
|
|
|
|
# Everything else: normal k=v colon strings
|
|
elif ":" in line:
|
|
field, value = line.split(":", 1)
|
|
if field in replacements:
|
|
field = replacements[field]
|
|
field = field.lower()
|
|
info[field] = value.strip()
|
|
|
|
result[name] = info
|
|
|
|
return result
|
|
|
|
|
|
def parse_STATS(data):
|
|
# BCCE Version Only
|
|
# BCCE Splits stats into its own section that we need to parse, return, then snap together
|
|
result = {}
|
|
|
|
# This is pretty identical to CHARACTERS
|
|
# Each character has a blank line between them
|
|
# Most everything else is k : v
|
|
for c_text in data.split("\n\n"):
|
|
name = "NULL"
|
|
c_data = {}
|
|
|
|
for line in c_text.split("\n"):
|
|
# Character name
|
|
if line[0:2].isdigit():
|
|
name = line[4:]
|
|
# Should be nothing, but let's be safe
|
|
elif ":" not in line:
|
|
pass
|
|
# A stat we can just save k : v
|
|
else:
|
|
stat, value = line.split(":")
|
|
c_data[stat] = int(value)
|
|
|
|
if name != "NULL":
|
|
result[name] = c_data
|
|
|
|
return result
|
|
|
|
|
|
def parse_COMMANDS(data):
|
|
commands = {}
|
|
|
|
# We split by ------ which divides the command name from its data
|
|
# As a result we have to pull the last line from each block and remember
|
|
# it as the name of the command in the next block. Blorf :)
|
|
next_command_name = None
|
|
for c_data in data.split("\n-------\n"):
|
|
c_data_lines = [c_data_line.strip() for c_data_line in c_data.split("\n")]
|
|
if "" in c_data_lines:
|
|
c_data_lines.remove("")
|
|
if next_command_name:
|
|
command_string = "; ".join(c_data_lines[:-1])
|
|
|
|
# Clip trailing junk from inconsistent spoiler log generation
|
|
# as well as the join above
|
|
if command_string.endswith("; "):
|
|
command_string = command_string[:-2]
|
|
if command_string.endswith("."):
|
|
command_string = command_string[:-1]
|
|
|
|
# Clean up a couple of clumsy string cases from the join above
|
|
command_string = command_string.replace(".; ", ": ")
|
|
command_string = command_string.replace(" ", " ")
|
|
command_string = command_string.replace(":;", ":")
|
|
|
|
# Commit the command to the dict
|
|
commands[next_command_name] = command_string
|
|
|
|
next_command_name = c_data_lines[-1].lower()
|
|
|
|
return commands
|
|
|
|
|
|
def parse_SEED(data):
|
|
# This is a fake section injected by the file loader. It contains only the seed code
|
|
is_BCCE = True if data.startswith("CE") else False
|
|
|
|
# Normalize seed codes to BCEX format, removing spaces and replacing pipes with dots
|
|
seed = data.replace("|", ".").replace(" ", "")
|
|
|
|
return {"is_bcce": is_BCCE, "seed": seed}
|
|
|
|
|
|
def parse_SECRET_ITEMS(data):
|
|
# BCCE Only
|
|
# I have no idea what this is lol, dump it to a list for now
|
|
return [line for line in data.split("\n") if not line.startswith("---")]
|
|
|
|
|
|
def load(filename):
|
|
# Load our file, tokenize by section header (starting with ====)
|
|
with open(filename) as infile:
|
|
tok_data = infile.read().split("============================================================\n")
|
|
|
|
sections = {}
|
|
|
|
top_section = True
|
|
for s in tok_data:
|
|
# The top section needs special handling and contains only seed code
|
|
if top_section:
|
|
sections["SEED"] = s.split("\n", 1)[0][12:]
|
|
top_section = False
|
|
continue
|
|
|
|
# Everything else we just dump into named sections for now
|
|
section_header, section_data = s.split("\n", 1)
|
|
sections[section_header[5:]] = section_data
|
|
|
|
return sections
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sections = load(sys.argv[1])
|
|
|
|
data = {}
|
|
|
|
# This mess tries to run a function named parse_SECTION for each section,
|
|
# and just continues to the next section if one doesn't exist.
|
|
for k, v in sections.items():
|
|
try:
|
|
section_func = f"parse_{k.replace(' ', '_')}"
|
|
data[k] = globals()[section_func](v)
|
|
except KeyError:
|
|
continue
|
|
|
|
# Subkey CHARACTERS commands with COMMANDS data
|
|
# This turns lists of commands each character has into hashes where
|
|
# Command name => Textual desc of command
|
|
# Certain flags don't shuffle commands like this so we have to check
|
|
if "COMMANDS" in data:
|
|
for character, c_data in data["CHARACTERS"].items():
|
|
new_commands = {}
|
|
|
|
for command in c_data["commands"]:
|
|
new_commands[command] = data["COMMANDS"].get(command, command)
|
|
c_data["commands"] = new_commands
|
|
|
|
# If we have a STATS block, snap it into CHARACTER data
|
|
# BCCE broke this out into its own section
|
|
# Worse, it keys on slot name, not randomized character name
|
|
if "STATS" in data:
|
|
for slot, stats in data["STATS"].items():
|
|
for c_name, c_data in data["CHARACTERS"].items():
|
|
if c_data["originally"].lower() == slot.lower():
|
|
c_data["stats"] = stats
|
|
del data["STATS"]
|
|
|
|
# If we ran BCCE Remonsterate, fold sprite data into monster block
|
|
if "REMONSTERATE" in data:
|
|
for name, info in data["REMONSTERATE"].items():
|
|
for m_name, m_info in data["MONSTERS"].items():
|
|
if name == m_name:
|
|
m_info["originally"] = info["originally"]
|
|
m_info["sprite"] = info["sprite"]
|
|
|
|
del data["REMONSTERATE"]
|
|
|
|
# Barf this pile of trash out
|
|
print(json.dumps(data))
|