From c782c3a79baf40203e293a1718f85b453f089ee7 Mon Sep 17 00:00:00 2001 From: Trysdyn Black Date: Wed, 9 Oct 2024 15:47:15 -0700 Subject: [PATCH 01/17] Make character command data consistent typing Previously a character's command data could be a list or a dict depending on if command randomization was on. This resulted in having to inspect the object and determine its type in tools using the output. This change guarantees character command data will be a dict of name->description. The description is pulled from the COMMANDS section of the spoiler log. If it doesn't exist (or the command is not present, which should never happen), the description will simply be the command name again. --- main.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/main.py b/main.py index 18891a3..7d0774d 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ """Parse BCEX (or BCCE) logs into json objects.""" -__version__ = "0.3" +__version__ = "0.4.0" __author__ = "Trysdyn Black" import json @@ -126,9 +126,14 @@ def parse_CHARACTERS(data: str) -> dict[str, dict]: # noqa: C901 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 + # Command list + # Commands: is just a CSV list of things like "fight" "magic" etc. However if command + # randomization is on, these will be exotic things like "KitMerton" that need additional + # info provided from the COMMANDS section later. + # As such, we begin with a dehydrated hash of command_name=None, then the COMMANDS section + # will provide hydration data for the values. elif line.startswith("Commands:"): - info["commands"] = [command.strip() for command in line.split(":")[1].split(",")] + info["commands"] = {command.strip(): None for command in line.split(":")[1].split(",")} elif line.startswith("Notable"): info["equipment"] = [eq.strip() for eq in line.split(":")[1].split(",")] @@ -288,17 +293,13 @@ if __name__ == "__main__": 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 c_data in data["CHARACTERS"].values(): - new_commands = {} - - for command in c_data["commands"]: - new_commands[command] = data["COMMANDS"].get(command, command) - c_data["commands"] = new_commands + # Hydrate each character's command list with descriptions of the commands + # This uses the COMMANDS section, but if one doesn't exist just repeat the command + # name because it should be simple things like "fight" and "magic" + command_info = data.get("COMMANDS", {}) + for c_data in data["CHARACTERS"].values(): + for command in c_data["commands"]: + c_data["commands"][command] = command_info.get(command, command) # If we have a STATS block, snap it into CHARACTER data # BCCE broke this out into its own section From 4b72709ae8743cadda647a439322c70c7af4eee1 Mon Sep 17 00:00:00 2001 From: Trysdyn Black Date: Wed, 9 Oct 2024 16:06:13 -0700 Subject: [PATCH 02/17] Small formatting changes --- main.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 7d0774d..dd1acfc 100644 --- a/main.py +++ b/main.py @@ -82,7 +82,6 @@ def parse_REMONSTERATE(data: str) -> dict[str, dict]: 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 @@ -112,8 +111,7 @@ def parse_CHARACTERS(data: str) -> dict[str, dict]: # noqa: C901 if line[0:2].isdigit(): name = line[4:] - # Stat chart rows - # BCEX Version Only + # Stat chart rows: BCEX Version only elif line.startswith("|"): for stat in line[1:-1].split("|"): if ":" in stat: @@ -309,6 +307,7 @@ if __name__ == "__main__": for c_data in data["CHARACTERS"].values(): 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 From eb924c14c2e70038fc55af69a9d3ed3b8d60626b Mon Sep 17 00:00:00 2001 From: Trysdyn Black Date: Wed, 9 Oct 2024 16:32:08 -0700 Subject: [PATCH 03/17] Change how we handle seed codes Rather than "normalize" seed codes to BCEX format (which causes issues with delimiter usage in some scenarios), leave the seed code alone and break its data out into fields that can/should be parsed instead. --- main.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/main.py b/main.py index dd1acfc..f933698 100644 --- a/main.py +++ b/main.py @@ -234,13 +234,19 @@ def parse_SEED(data: str) -> dict[str, bool | str]: This is a fake section injected by the loader code. It contains nothing but the seed code and we derive from this if the randomizer is BCCE or - BCEX, and normalize the seed code to a standard format by undoing the - changes BCCE makes to it. - """ - # Normalize seed codes to BCEX format, removing spaces and replacing pipes with dots - seed = data.replace("|", ".").replace(" ", "") + BCEX, and try to pluck out other data. - return {"is_bcce": data.startswith("CE"), "seed": seed} + We can't do much because the format is really hard to reverse. + """ + version, mode, flags, seed_num = data.split("|") if "|" in data else data.split(".") + return { + "version": version, + "flags": flags, + "seed_num": seed_num, + "mode": mode, + "is_bcce": data.startswith("CE"), + "seed": data, + } def parse_SECRET_ITEMS(data: str) -> list[str]: From 14ec718890c0a5939d18db975a36b38ff9a5c3fd Mon Sep 17 00:00:00 2001 From: Trysdyn Black Date: Fri, 11 Oct 2024 17:45:31 -0700 Subject: [PATCH 04/17] Refactor entire script into a class - Shove all the logic in a class - Allow for `cleanup_SECTION` functions, one that returns true will delete the calling section in post-run - Get rid of that awful awful `globals()` call, which was the main motivation of this - Document the new methods This should result in a horrifying diff that claims 98% of the file has changed but no actual change in logic or output. --- main.py | 619 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 332 insertions(+), 287 deletions(-) diff --git a/main.py b/main.py index f933698..b60011c 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ """Parse BCEX (or BCCE) logs into json objects.""" -__version__ = "0.4.0" +__version__ = "0.4.1" __author__ = "Trysdyn Black" import json @@ -10,321 +10,366 @@ import sys from pathlib import Path -def parse_MONSTERS(data: str) -> dict[str, dict]: # noqa: C901, PLR0912 - """ - Parse the MONSTERS section. +class Parser: + """BCEX/BCCE spoiler logfile parser.""" - This contains data on monsters including stat sheets, loot, and weaknesses. - """ - 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(";") + def __init__(self, filename: str) -> None: + """Initialize parser with filename.""" + self.filename = filename + self.config_sections = {} + self.data_sections = {} + + @staticmethod + def parse_MONSTERS(data: str) -> dict[str, dict]: # noqa: C901, PLR0912 + """ + Parse the MONSTERS section. + + This contains data on monsters including stat sheets, loot, and weaknesses. + """ + 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(", ") + # 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"] = {} + # Everything else is a simple k: v list where v is comma-delimited else: - null_text = line - weak_text = "WEAK: " - info["nullifies"] = null_text.split(": ")[1].split(", ") - info["weak"] = weak_text.split(": ")[1].split(", ") - # 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} + for k in ["immune", "auto", "skills", "steal", "drops", "location"]: + str_match = f"{k.upper()}:" + if line.startswith(str_match): + info[k] = line.split(": ")[1].split(", ") if line.upper().strip() != str_match else [] + break + + if name != "NULL": + result[name] = info + + return result + + @staticmethod + def parse_REMONSTERATE(data: str) -> dict[str, dict]: + """ + Parse the BCCE-only REMONSTERATE section. + + This contains a mapping of monster sprites: what they were and what they + turned into post-alteration. + """ + 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 + + @staticmethod + def parse_CHARACTERS(data: str) -> dict[str, dict]: # noqa: C901 + """ + Parse the CHARACTERS section. + + This differs based on BCEX vs BCCE. In both flavors it contains basic data + like name, spells, location and special abilities. In BCEX it includes + stats as well. In BCCE stats is its own section. + + Regardless of flavor, core logic will snap stats back into this section + later. + """ + 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]) + + # Command list + # Commands: is just a CSV list of things like "fight" "magic" etc. However if command + # randomization is on, these will be exotic things like "KitMerton" that need additional + # info provided from the COMMANDS section later. + # As such, we begin with a dehydrated hash of command_name=None, then the COMMANDS section + # will provide hydration data for the values. + elif line.startswith("Commands:"): + info["commands"] = {command.strip(): None 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 + + @staticmethod + def parse_STATS(data: str) -> dict[str, dict]: + """ + Parse the BCCE-only STATS section. + + BCCE splits character stats into its own section. We use largely the same + logic as CHARACTERS here to parse it, then return it as its own dict for + merging back into the CHARACTERS blob later. + """ + 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: - info["special"] = {} - # Everything else is a simple k: v list where v is comma-delimited - else: - for k in ["immune", "auto", "skills", "steal", "drops", "location"]: - str_match = f"{k.upper()}:" - if line.startswith(str_match): - info[k] = line.split(": ")[1].split(", ") if line.upper().strip() != str_match else [] - break + stat, value = line.split(":") + c_data[stat] = int(value) - if name != "NULL": - result[name] = info + if name != "NULL": + result[name] = c_data - return result + return result + @staticmethod + def parse_COMMANDS(data: str) -> dict[str, dict]: + """ + Parse the COMMANDS section. -def parse_REMONSTERATE(data: str) -> dict[str, dict]: - """ - Parse the BCCE-only REMONSTERATE section. + This contains information on special commands, expanding contracted command + names into more detailed explanations like GranSaw = Grand Train + Chainsaw. + """ + commands = {} - This contains a mapping of monster sprites: what they were and what they - turned into post-alteration. - """ - 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} + # 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]) - return result + # 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(":;", ":") -def parse_CHARACTERS(data: str) -> dict[str, dict]: # noqa: C901 - """ - Parse the CHARACTERS section. + # Commit the command to the dict + commands[next_command_name] = command_string - This differs based on BCEX vs BCCE. In both flavors it contains basic data - like name, spells, location and special abilities. In BCEX it includes - stats as well. In BCCE stats is its own section. + next_command_name = c_data_lines[-1].lower() - Regardless of flavor, core logic will snap stats back into this section - later. - """ - replacements = {"Looks like": "looks", "World of Ruin location": "wor_location", "Notable equipment": "equipment"} + return commands - result = {} + @staticmethod + def parse_SEED(data: str) -> dict[str, bool | str]: + """ + Parse the injected SEED section. - for c_data in data.split("\n\n")[1:-1]: - info = {"stats": {}, "spells": {}, "natural_magic": False} - name = "NULL" + This is a fake section injected by the loader code. It contains nothing + but the seed code and we derive from this if the randomizer is BCCE or + BCEX, and try to pluck out other data. - for line in c_data.split("\n"): - # Name - if line[0:2].isdigit(): - name = line[4:] + We can't do much because the format is really hard to reverse. + """ + version, mode, flags, seed_num = data.split("|") if "|" in data else data.split(".") + return { + "version": version, + "flags": flags, + "seed_num": seed_num, + "mode": mode, + "is_bcce": data.startswith("CE"), + "seed": data, + } - # 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) + @staticmethod + def parse_SECRET_ITEMS(data: str) -> list[str]: + """ + Parse the BCCE-only SECRET ITEMS section. - # 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]) + I'm unsure what this is for. It's a series of strings with no real obvious + significance, so we just return it as a list. + """ + # 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("---")] - # Command list - # Commands: is just a CSV list of things like "fight" "magic" etc. However if command - # randomization is on, these will be exotic things like "KitMerton" that need additional - # info provided from the COMMANDS section later. - # As such, we begin with a dehydrated hash of command_name=None, then the COMMANDS section - # will provide hydration data for the values. - elif line.startswith("Commands:"): - info["commands"] = {command.strip(): None for command in line.split(":")[1].split(",")} + @staticmethod + def cleanup_STATS(data: dict) -> bool: + """ + Fold BCCE-only STATS section back into CHARACTERS data. - elif line.startswith("Notable"): - info["equipment"] = [eq.strip() for eq in line.split(":")[1].split(",")] + This returns BCCE logs back to how they were laid out in BCEX: where stat blocks + were simply part of the CHARACTERS data. - # 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: str) -> dict[str, dict]: - """ - Parse the BCCE-only STATS section. - - BCCE splits character stats into its own section. We use largely the same - logic as CHARACTERS here to parse it, then return it as its own dict for - merging back into the CHARACTERS blob later. - """ - 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: str) -> dict[str, dict]: - """ - Parse the COMMANDS section. - - This contains information on special commands, expanding contracted command - names into more detailed explanations like GranSaw = Grand Train + Chainsaw. - """ - 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: str) -> dict[str, bool | str]: - """ - Parse the injected SEED section. - - This is a fake section injected by the loader code. It contains nothing - but the seed code and we derive from this if the randomizer is BCCE or - BCEX, and try to pluck out other data. - - We can't do much because the format is really hard to reverse. - """ - version, mode, flags, seed_num = data.split("|") if "|" in data else data.split(".") - return { - "version": version, - "flags": flags, - "seed_num": seed_num, - "mode": mode, - "is_bcce": data.startswith("CE"), - "seed": data, - } - - -def parse_SECRET_ITEMS(data: str) -> list[str]: - """ - Parse the BCCE-only SECRET ITEMS section. - - I'm unsure what this is for. It's a series of strings with no real obvious - significance, so we just return it as a list. - """ - # 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: str) -> dict[str, str]: - """Load file and tokenize into sections.""" - # Load our file, tokenize by section header (starting with ====) - with Path(filename).open(encoding="utf-8") 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 - - # Hydrate each character's command list with descriptions of the commands - # This uses the COMMANDS section, but if one doesn't exist just repeat the command - # name because it should be simple things like "fight" and "magic" - command_info = data.get("COMMANDS", {}) - for c_data in data["CHARACTERS"].values(): - for command in c_data["commands"]: - c_data["commands"][command] = command_info.get(command, command) - - # 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_data in data["CHARACTERS"].values(): + The BCCE STATS section keys on character slot (Terra, Locke, etc) and not the + new randomized character name, so some hunting has to happen here. + """ + for slot, stats in data.get("STATS", {}).items(): + for c_data in data.get("CHARACTERS", {}).values(): if c_data["originally"].lower() == slot.lower(): c_data["stats"] = stats - del data["STATS"] + return True - # 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(): + @staticmethod + def cleanup_COMMANDS(data: dict) -> bool: + """Fold COMMANDS expanded descriptions into CHARACTERS command data.""" + # If our COMMANDS section is missing or somehow missing a given command, we just + # repeat the command's name as its description. This should only be simple things + # like "fight" and "magic" unless something goes wrong. + command_info = data.get("COMMANDS", {}) + for c_data in data.get("CHARACTERS", {}).values(): + for command in c_data.get("commands", {}): + c_data["commands"][command] = command_info.get(command, command) + + return False + + @staticmethod + def cleanup_REMONSTERATE(data: dict) -> bool: + """Fold REMONSTERATE section into MONSTERS section data.""" + for name, info in data.get("REMONSTERATE", {}).items(): + for m_name, m_info in data.get("MONSTERS", {}).items(): if name == m_name: m_info["originally"] = info["originally"] m_info["sprite"] = info["sprite"] - del data["REMONSTERATE"] + return True + + def get_sections(self, data: str) -> dict[str, str]: + """Split logfile text and return a dict of sections for parsing.""" + tok_data = data.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 + + self.config_sections = sections + return sections + + def parse(self) -> dict: + """Fully parse the logfile and return the full data object.""" + # Get individual sections to work on + with Path(self.filename).open(encoding="utf-8") as infile: + sections = self.get_sections(infile.read()) + + data = {} + + # For each section attempt to run a parser function for it + for k, v in sections.items(): + section_func = f"parse_{k.replace(' ', '_')}" + if hasattr(self, section_func): + data[k] = getattr(self, section_func)(v) + + # Do post-parse cleanup. We need all sections parsed to do these + # Any cleanup function that returns true has its respective section deleted + section_dels = set() + for k in data: + section_func = f"cleanup_{k.replace(' ', '_')}" + if hasattr(self, section_func) and getattr(self, section_func)(data): + section_dels.add(k) + + # Any section cleanup that returns true means delete that section + for k in section_dels: + if k in data: + del data[k] + + self.data_sections = data + return data + + +if __name__ == "__main__": + p = Parser(sys.argv[1]) + data = p.parse() # Barf this pile of trash out print(json.dumps(data)) From cedf27485aa96604f884608059d9f55a10ca0d93 Mon Sep 17 00:00:00 2001 From: Trysdyn Black Date: Fri, 11 Oct 2024 17:57:27 -0700 Subject: [PATCH 05/17] Note unsupported log sections --- README.md | 16 ++++++++++++++++ main.py | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 56f8e6f..4bff271 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,19 @@ Additionally the tool only parses spoiler log data needed for known use-cases, s BCCE (The community revival of the BCEX project) is supported, but support is geared toward taking BCCE's spoiler logs and producing identical output to BCEX. This means stats are not their own data object, but are folded into character data just like BCEX outputs it. Remonsterate is supported and inserts its data into the monsters object. BCCE is in active development and this may break at any time; see the first paragraph in this section. + +## Known Support Gaps + +These sections in the BCEX/BCCE spoiler logs currently have no logic and I'm aware of it. That doesn't mean sections *not* listed here have support; they may not and I'm not aware of them. + +- AESTHETICS +- MAGITEK +- DANCES +- ESPERS +- ITEM MAGIC +- ITEM EFFECTS +- COLOSSEUM +- MUSIC +- SHOPS +- TREASURE CHESTS +- JUNCTIONS diff --git a/main.py b/main.py index b60011c..a0f3b37 100644 --- a/main.py +++ b/main.py @@ -11,7 +11,22 @@ from pathlib import Path class Parser: - """BCEX/BCCE spoiler logfile parser.""" + """ + BCEX/BCCE spoiler logfile parser. + + Sections missing support: + - AESTHETICS + - MAGITEK + - DANCES + - ESPERS + - ITEM MAGIC + - ITEM EFFECTS + - COLOSSEUM + - MUSIC + - SHOPS + - TREASURE CHESTS + - JUNCTIONS + """ def __init__(self, filename: str) -> None: """Initialize parser with filename.""" From e4af5dcf3bf0cd7a20134250c51874cdd2b633ca Mon Sep 17 00:00:00 2001 From: Trysdyn Black Date: Fri, 11 Oct 2024 18:00:39 -0700 Subject: [PATCH 06/17] Note in README to use a release and not HEAD --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4bff271..b969353 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ FF6 BCEX Spoiler Log to JSON Parser This tool uses only the Python standard library; no packages are needed. +Grab the latest release from the "Releases" section of the source repo, then unzip and run: + `./main.py ` It's recommended to pipe the output to `jq` or a similar json parsing tool. From ae9451f336b63f603678f3dafa22dd0feecd868f Mon Sep 17 00:00:00 2001 From: Trysdyn Black Date: Fri, 11 Oct 2024 21:38:46 -0700 Subject: [PATCH 07/17] Support MUSIC and AESTHETICS sections --- README.md | 2 -- main.py | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b969353..a9672d8 100644 --- a/README.md +++ b/README.md @@ -34,14 +34,12 @@ BCCE is in active development and this may break at any time; see the first para These sections in the BCEX/BCCE spoiler logs currently have no logic and I'm aware of it. That doesn't mean sections *not* listed here have support; they may not and I'm not aware of them. -- AESTHETICS - MAGITEK - DANCES - ESPERS - ITEM MAGIC - ITEM EFFECTS - COLOSSEUM -- MUSIC - SHOPS - TREASURE CHESTS - JUNCTIONS diff --git a/main.py b/main.py index a0f3b37..f31a827 100644 --- a/main.py +++ b/main.py @@ -15,14 +15,12 @@ class Parser: BCEX/BCCE spoiler logfile parser. Sections missing support: - - AESTHETICS - MAGITEK - DANCES - ESPERS - ITEM MAGIC - ITEM EFFECTS - COLOSSEUM - - MUSIC - SHOPS - TREASURE CHESTS - JUNCTIONS @@ -288,6 +286,85 @@ class Parser: # 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("---")] + @staticmethod + def parse_MUSIC(data: str) -> dict[str, dict]: + """ + Parse the MUSIC section. + + This differs significantly between BCEX and BCCE: Numbers and data are split + with a period in BCCE and a colon in BCEX. BCEX puts arranger on the same line + as composer, split with --, BCEX lacks the Jukebox Title section entirely. + """ + music_sections = data.split("\n\n") + + replacements = {} + + for section in music_sections[1:]: + if not section.strip(): + continue + + # BCEX and BCCE divide numbers from data differently + if ":" in section[:6]: + _, info = section.split(":", 1) + else: + _, info = section.split(".", 1) + + # The name of the song being replaced preceeds a -> + old_name, info = info.split("->", 1) + old_name = old_name.strip() + + replacements[old_name] = {} + + # Info is, mostly, one item per line, so let's go by line + tok_info = info.split("\n") + + for k in ("name", "title", "composer", "arranger", "jukebox_title"): + # Not every song has all the data + if not len(tok_info): + break + + line = tok_info.pop(0).strip() + + # BCCE puts arranger on its own line. BCEX puts it on the same line as + # composed, split by "--". So we have to handle both + if "-- Arranged by " in line: + line, arranger = line.split("-- Arranged by ", 1) + replacements[old_name]["arranger"] = arranger + + # Jukebox Title is in prens with extra stuff to chomp + if "Jukebox title" in line: + line = line[16:-1] + + # These strings are fluff but should only appear in compose/arranger lines + # so it's safe to just blindly chomp them + line = line.split("Composed by ", 1)[-1].strip() + line = line.replace("Ripped and/or arranged by ", "") + + replacements[old_name][k] = line + + return replacements + + @staticmethod + def parse_AESTHETICS(data: str) -> dict[str, str]: + """ + Parse the BCCE-only AESTHETICS section. + + This is just a k=v list we split up and strip. + """ + replacements = {} + + for line in data.split("\n"): + if ":" not in line: + continue + + old, new = line.split(":") + old = old.strip() + new = new.strip() + + replacements[old] = new + + return replacements + @staticmethod def cleanup_STATS(data: dict) -> bool: """ From 6a0ad9c7eab221c3681367ba5367ca965b085368 Mon Sep 17 00:00:00 2001 From: Trysdyn Black Date: Sun, 13 Oct 2024 02:14:11 -0700 Subject: [PATCH 08/17] Support five new log sections We add support for... - MAGITEK - DANCES - ESPERS - ITEM MAGIC - COLOSSEUM --- README.md | 5 -- main.py | 157 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 151 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a9672d8..be4dce8 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,7 @@ BCCE is in active development and this may break at any time; see the first para These sections in the BCEX/BCCE spoiler logs currently have no logic and I'm aware of it. That doesn't mean sections *not* listed here have support; they may not and I'm not aware of them. -- MAGITEK -- DANCES -- ESPERS -- ITEM MAGIC - ITEM EFFECTS -- COLOSSEUM - SHOPS - TREASURE CHESTS - JUNCTIONS diff --git a/main.py b/main.py index f31a827..f47d2f9 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ """Parse BCEX (or BCCE) logs into json objects.""" -__version__ = "0.4.1" +__version__ = "0.4.2" __author__ = "Trysdyn Black" import json @@ -15,12 +15,7 @@ class Parser: BCEX/BCCE spoiler logfile parser. Sections missing support: - - MAGITEK - - DANCES - - ESPERS - - ITEM MAGIC - ITEM EFFECTS - - COLOSSEUM - SHOPS - TREASURE CHESTS - JUNCTIONS @@ -365,6 +360,156 @@ class Parser: return replacements + @staticmethod + def parse_MAGITEK(data: str) -> dict[str, list]: + """ + Parse the BCCE-only MAGITEK section. + + This section contains info on what Terra and the guards have in their magitek skill + lists at the start of the game. Unfortunately like the STATS block, this keys on slot + name and not randomized name, but at least it's super simple. + """ + result = {"terra": [], "others": []} + + mode = None + for line in data.split("\n"): + if line.startswith("Terra Magitek"): + mode = "terra" + elif line.startswith("Other Actor"): + mode = "others" + elif line.strip() and mode: + result[mode].append(line.strip()) + + return result + + @staticmethod + def parse_DANCES(data: str) -> dict[str, dict]: + """ + Parse the BCCE-only DANCES section. + + This section's a list of dances with their effects chances. Each dance seems to + always have four effects, spaced at specific locations in the string, so we chomp + by hardcoded locations. Brittle. + """ + result = {} + + dance = None + for line in data.split("\n"): + # Formatting line + if "-----" in line: + continue + # Result list + if line.startswith(" "): + # This is more brittle than I'd like but the logs space results out by + # character position and some dance results have spaces so it's hard to + # split properly. + for i in range(2, 57, 18): + chance, effect = line[i : i + 18].split(" ", 1) + result[dance][effect.strip()] = chance.strip() + # New dance section + elif line.strip(): + dance = line.strip() + if dance not in result: + result[dance] = {} + + return result + + @staticmethod + def parse_ESPERS(data: str) -> dict[str, dict]: + """ + Parse the ESPERS section. + + This section lists espers, what they teach, their bonuses, and locations. We + assume any line with a : in it is either BONUS or LOCATION and just k=v it. Any + line after a blank line is an esper name, and anything else is a spell learn option. + """ + result = {} + + esper = None + next_esper = False + for line in data.split("\n"): + # Formatting line + if "-----" in line: + continue + # Blank lines divide esper sections + if not line.strip(): + next_esper = True + # The first line in a new section is the esper name + elif next_esper: + esper = line.strip() + if esper not in result: + result[esper] = {"learnset": {}} + next_esper = False + # Any line with ":" is a k=v we should just shove into the dict + elif ": " in line: + k, v = line.split(": ") + result[esper][k.lower()] = v.strip() + # Everything else should be spell learnset + else: + spell, mult = line.split(" x") + result[esper]["learnset"][spell.strip()] = f"x{mult}" + + return result + + @staticmethod + def parse_ITEM_MAGIC(data: str) -> dict[str, dict]: + """ + Parse the ITEM MAGIC section. + + This section is actually three distinct subsections. Breakable items and procs + we can just store as k=v in sub-dicts. The spell-teaching items section we treat + like the esper learnset and store spell_name = learn multiplier. + """ + result = {} + + section = None + for line in data.split("\n"): + # Formatting lines + if "-----" in line or not line.strip(): + continue + # Anything with ":" is a k=v to insert + if ":" in line: + k, v = line.split(": ") + # Spell-teaching needs special logic to get the {spell: multiplier} format + if section == "spell-teaching": + spell, mult = v.split(" x") + result[section][k.strip()] = {spell.strip(): f"x{mult}"} + else: + result[section][k.strip()] = v.strip() + # Anything else should be a section header. Use the first word unless it's "ITEM" + else: + tok_line = line.split() + section = tok_line[1].lower() if "ITEM" in tok_line[0] else tok_line[0].lower() + result[section] = {} + + return result + + @staticmethod + def parse_COLOSSEUM(data: str) -> dict[str, dict]: + """ + Parse the COLOSSEUM section. + + Each line is one item offered, which has a resulting item, a monster level, and a + a monster name. This results in basic string splitting giving us all the data we + need. + """ + result = {} + + for line in data.split("\n"): + if "-----" in line or not line.strip(): + continue + + item, tok_line = line.split("->") + new_item, tok_line = tok_line.split(": LV ") + level, name = tok_line.split(" ", 1) + + result[item.strip()] = { + "becomes": new_item.strip(), + "battle": {"monster": name.strip(), "level": int(level.strip())}, + } + + return result + @staticmethod def cleanup_STATS(data: dict) -> bool: """ From 53c958f087fedf20d3932beead3de4b254e0b59e Mon Sep 17 00:00:00 2001 From: Trysdyn Black Date: Sun, 13 Oct 2024 02:34:33 -0700 Subject: [PATCH 09/17] Support MORPH fields in monster data --- main.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/main.py b/main.py index f47d2f9..a236a75 100644 --- a/main.py +++ b/main.py @@ -72,6 +72,13 @@ class Parser: info["special"] = {special_name: special_desc} else: info["special"] = {} + # Morph results, with a percent chance in each one + elif line.startswith("MORPH"): + _, chance, items = line.split(" ", 2) + chance = int(chance[1:-3]) + items = items.split(", ") + if "morph" not in info: + info["morph"] = {"percent_chance": chance, "items": items} # Everything else is a simple k: v list where v is comma-delimited else: for k in ["immune", "auto", "skills", "steal", "drops", "location"]: From ed29b321736ecd551d803c1622b26aab202b000e Mon Sep 17 00:00:00 2001 From: Trysdyn Black Date: Sun, 13 Oct 2024 16:02:27 -0700 Subject: [PATCH 10/17] Add support for ITEM EFFECTS section --- README.md | 1 - main.py | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index be4dce8..3a2db6b 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,6 @@ BCCE is in active development and this may break at any time; see the first para These sections in the BCEX/BCCE spoiler logs currently have no logic and I'm aware of it. That doesn't mean sections *not* listed here have support; they may not and I'm not aware of them. -- ITEM EFFECTS - SHOPS - TREASURE CHESTS - JUNCTIONS diff --git a/main.py b/main.py index a236a75..f8145d5 100644 --- a/main.py +++ b/main.py @@ -10,12 +10,11 @@ import sys from pathlib import Path -class Parser: +class Parser: # noqa: PLR0904 """ BCEX/BCCE spoiler logfile parser. Sections missing support: - - ITEM EFFECTS - SHOPS - TREASURE CHESTS - JUNCTIONS @@ -501,6 +500,7 @@ class Parser: need. """ result = {} + mode = None for line in data.split("\n"): if "-----" in line or not line.strip(): @@ -517,6 +517,68 @@ class Parser: return result + @staticmethod + def parse_ITEM_EFFECTS(data: str) -> dict[str, dict]: # noqa: C901, PLR0912 + """ + Parse the BCCE-only ITEM EFFECTS section. + + This is a weird chimera section with multiple sub-sections. Most of these + sub-sections can just be k:v or k:v, v, v parsed. Elemental properties is + a special indented list and special features can contain command changers + so we need a lot of special parsing. + """ + result = {} + mode = None + item = None + + for line in data.split("\n"): + if "-----" in line or not line.strip(): + continue + + if line == line.upper(): + mode = line.lower().strip().replace(" ", "_") + result[mode] = {} + # Everything here is k:v splitable except the elemental properties + # section which is k:v but with a return and indent and multiple values. + elif mode == "elemental_properties": + if line.startswith(" "): + tok_line = line.split() + operator = "+" if "Gained" in tok_line else "-" if "Lost" in tok_line else "" + element = tok_line[-1] + effect = tok_line[2].strip(":") + + if item not in result[mode]: + result[mode][item] = [] + + result[mode][item].append(f"{operator}{element} {effect}") + else: + item = line.split(":")[0] + result[mode][item] = [] + else: + item, effect = line.split(":") + item = item.strip() + effect = effect.strip() + + for effect_token in effect.split(", "): + # This is a command change. This can appear in command_changers *or* features + # In either case the result goes in command changers for consistency + if "->" in effect_token: + old, new = effect_token.split("->") + # We don't have a guarantee COMMAND CHANGERS is in yet... + if "command_changers" not in result: + result["command_changers"] = {} + if item not in result["command_changers"]: + result["command_changers"][item] = {} + + result["command_changers"][item][old.strip()] = new.strip() + # Everything else should hopefully just be a basic effect list + else: + if item not in result[mode]: + result[mode][item] = [] + result[mode][item].append(effect_token.strip()) + + return result + @staticmethod def cleanup_STATS(data: dict) -> bool: """ From 5234a6bd1536a20310e0e12c5d191683b160a24b Mon Sep 17 00:00:00 2001 From: Trysdyn Black Date: Sun, 13 Oct 2024 16:15:33 -0700 Subject: [PATCH 11/17] Support SHOPS section Though its format is not entirely to my liking. --- README.md | 1 - main.py | 31 ++++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3a2db6b..ca55bd1 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,5 @@ BCCE is in active development and this may break at any time; see the first para These sections in the BCEX/BCCE spoiler logs currently have no logic and I'm aware of it. That doesn't mean sections *not* listed here have support; they may not and I'm not aware of them. -- SHOPS - TREASURE CHESTS - JUNCTIONS diff --git a/main.py b/main.py index f8145d5..fad53b5 100644 --- a/main.py +++ b/main.py @@ -15,7 +15,6 @@ class Parser: # noqa: PLR0904 BCEX/BCCE spoiler logfile parser. Sections missing support: - - SHOPS - TREASURE CHESTS - JUNCTIONS """ @@ -579,6 +578,36 @@ class Parser: # noqa: PLR0904 return result + @staticmethod + def parse_SHOPS(data: str) -> dict[str, dict]: + """ + Parse SHOPS section. + + For now this is just a dict of shop name => {item => cost}. I would like to split + this up better so you have, for example narshe["wob"]["after_kefka"]["weapons"] but + that's probably more lifting than is necessary. Most people will just be searching + for buyable items period. + """ + result = {} + shop = None + + for line in data.split("\n"): + if "-----" in line or not line.strip(): + continue + + if line == line.upper(): + shop = line.strip() + result[shop] = {"stock": {}, "female_discount": False} + elif line.startswith("Discounts for female characters"): + result[shop]["female_discount"] = True + else: + tok_line = line.split() + item = " ".join(tok_line[:-1]) + cost = tok_line[-1] + result[shop]["stock"][item.strip()] = int(cost.strip()) + + return result + @staticmethod def cleanup_STATS(data: dict) -> bool: """ From 51cd2f03ccd7f5ed68cd8699adbfa28ea82fc2ce Mon Sep 17 00:00:00 2001 From: Trysdyn Black Date: Sun, 13 Oct 2024 16:17:17 -0700 Subject: [PATCH 12/17] Bump version to 0.5.0 --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index fad53b5..58f4124 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ """Parse BCEX (or BCCE) logs into json objects.""" -__version__ = "0.4.2" +__version__ = "0.5.0" __author__ = "Trysdyn Black" import json From f733ebb66d1282f5eec0eec42730c800b7b1c3c1 Mon Sep 17 00:00:00 2001 From: Trysdyn Black Date: Wed, 16 Oct 2024 00:28:48 -0700 Subject: [PATCH 13/17] Support BCEX 5.0 - We have to work around a bug where morph chances have double % - The skill names in monster specials got changed to single-quoted instead of double quoted, go figure It runs; a quick peek makes it look like everything's okay. There'll be bugs I'm sure. --- README.md | 2 ++ main.py | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ca55bd1..3779e65 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ The parser is quite brittle since the inconsistency of the original spoiler log Additionally the tool only parses spoiler log data needed for known use-cases, so if you plan to use it you may need to request the inclusion of spoiler log sections. The tool can be expanded by adding new functions named `parse_SECTION` where `SECTION` is the full name of a section in the log, as presented in the log. +Development targed BCEX 4.0, but BCEX 5.0 is in limited support. It works but all the bugs haven't been found yet. + BCCE (The community revival of the BCEX project) is supported, but support is geared toward taking BCCE's spoiler logs and producing identical output to BCEX. This means stats are not their own data object, but are folded into character data just like BCEX outputs it. Remonsterate is supported and inserts its data into the monsters object. BCCE is in active development and this may break at any time; see the first paragraph in this section. diff --git a/main.py b/main.py index 58f4124..1394bee 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ """Parse BCEX (or BCCE) logs into json objects.""" -__version__ = "0.5.0" +__version__ = "0.5.1" __author__ = "Trysdyn Black" import json @@ -65,7 +65,8 @@ class Parser: # noqa: PLR0904 elif line.startswith("SPECIAL"): content = line.split(" ", 1)[1] if len(content) > 1: - special_name = content.split('"')[1] + # BCEX 5.0 changes the skill name to be single quoted instead of double + special_name = content.split('"')[1] if '"' in content else content.split("'")[1] special_desc = content.split(": ")[1] info["special"] = {special_name: special_desc} else: @@ -73,7 +74,8 @@ class Parser: # noqa: PLR0904 # Morph results, with a percent chance in each one elif line.startswith("MORPH"): _, chance, items = line.split(" ", 2) - chance = int(chance[1:-3]) + # BCEX 5.0 has a bug where % can be doubled sometimes + chance = int(chance[1:-3].replace("%", "")) items = items.split(", ") if "morph" not in info: info["morph"] = {"percent_chance": chance, "items": items} From 82ff5f86f4449b8c1fa14106894ed6c4eae7c0c4 Mon Sep 17 00:00:00 2001 From: Trysdyn Black Date: Wed, 16 Oct 2024 22:58:57 -0700 Subject: [PATCH 14/17] Fix formatting oddness for foes with no weakness --- main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/main.py b/main.py index 1394bee..7b2798d 100644 --- a/main.py +++ b/main.py @@ -60,6 +60,11 @@ class Parser: # noqa: PLR0904 weak_text = "WEAK: " info["nullifies"] = null_text.split(": ")[1].split(", ") info["weak"] = weak_text.split(": ")[1].split(", ") + + # Due to split oddness we can populate a blank string into weakness + # Delete it if we did. + if info["weak"] == [""]: + del info["weak"] # Specials are name=>desc as k:v # I *think* you can only have one special... elif line.startswith("SPECIAL"): From 1af6a89e6b20bf939693caaf7c8ec003a9b7e19f Mon Sep 17 00:00:00 2001 From: Trysdyn Black Date: Wed, 16 Oct 2024 23:09:59 -0700 Subject: [PATCH 15/17] Add names to large data objects This is so if you do a jq query like `.CHARACTERS[] | select(originally == "Mog")` you'll get their name in the data output. --- main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 7b2798d..3daaa48 100644 --- a/main.py +++ b/main.py @@ -43,6 +43,7 @@ class Parser: # noqa: PLR0904 if "(Level " in line: name = line.split(" (")[0] info["stats"]["level"] = int(line.split("(Level ")[1][:-1]) + info["name"] = name # Stat chart rows elif line.startswith("|"): for stat in line[1:-1].split("|"): @@ -144,6 +145,7 @@ class Parser: # noqa: PLR0904 # Name if line[0:2].isdigit(): name = line[4:] + info["name"] = name # Stat chart rows: BCEX Version only elif line.startswith("|"): @@ -422,7 +424,7 @@ class Parser: # noqa: PLR0904 elif line.strip(): dance = line.strip() if dance not in result: - result[dance] = {} + result[dance] = {"name": dance} return result @@ -450,7 +452,7 @@ class Parser: # noqa: PLR0904 elif next_esper: esper = line.strip() if esper not in result: - result[esper] = {"learnset": {}} + result[esper] = {"learnset": {}, "name": esper} next_esper = False # Any line with ":" is a k=v we should just shove into the dict elif ": " in line: From 8f74ce3a28f59f86dda1b84b24712b3cfd642f53 Mon Sep 17 00:00:00 2001 From: Trysdyn Black Date: Sat, 19 Oct 2024 17:40:07 -0700 Subject: [PATCH 16/17] Add license to prepare for use by another project --- LICENSE.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..b53c985 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,29 @@ +"i'm so tired" software license 1.0 + +copyright (c) 2024 Trysdyn Black + +this is anti-capitalist, anti-bigotry software, made by people who are tired of ill-intended organisations and individuals, and would rather not have those around their creations. + +permission is granted, free of charge, to any user (be they a person or an organisation) obtaining a copy of this software, to use it for personal, commercial, or educational purposes, subject to the following conditions: + +1. the above copyright notice and this permission notice shall be included in all copies or modified versions of this software. + +2. the user is one of the following: + a. an individual person, labouring for themselves + b. a non-profit organisation + c. an educational institution + d. an organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor + +3. if the user is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote. + +4. if the user is an organization, then the user is not law enforcement or military, or working for or under either. + +5. the user does not use the software for ill-intentioned reasons, as determined by the authors of the software. said reasons include but are not limited to: + a. bigotry, including but not limited to racism, xenophobia, homophobia, transphobia, ableism, sexism, antisemitism, religious intolerance + b. pedophilia, zoophilia, and/or incest + c. support for cops and/or the military + d. any blockchain-related technology, including but not limited to cryptocurrencies + +6. the user does not promote or engage with any of the activities listed in the previous item, and is not affiliated with any group that promotes or engages with any of such activities. + +this software is provided as is, without any warranty or condition. in no event shall the authors be liable to anyone for any damages related to this software or this license, under any kind of legal claim. From 03fb76e0fcb587f03a08cffa736c5cf450780f2e Mon Sep 17 00:00:00 2001 From: Trysdyn Black Date: Mon, 21 Oct 2024 20:33:32 -0700 Subject: [PATCH 17/17] Remove unused mode variable --- main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/main.py b/main.py index 3daaa48..0d16b67 100644 --- a/main.py +++ b/main.py @@ -508,7 +508,6 @@ class Parser: # noqa: PLR0904 need. """ result = {} - mode = None for line in data.split("\n"): if "-----" in line or not line.strip():