bcexparse/main.py

299 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 = {"stats": {}, "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))