bcexparse/main.py
Trysdyn Black db4091f41f Make pyright shut up about dicts
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.
2024-10-06 18:59:19 -07:00

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))