#!/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 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))