Compare commits
10 commits
ae9451f336
...
03fb76e0fc
Author | SHA1 | Date | |
---|---|---|---|
03fb76e0fc | |||
8f74ce3a28 | |||
1af6a89e6b | |||
82ff5f86f4 | |||
f733ebb66d | |||
51cd2f03cc | |||
5234a6bd15 | |||
ed29b32173 | |||
53c958f087 | |||
6a0ad9c7ea |
3 changed files with 292 additions and 17 deletions
29
LICENSE.md
Normal file
29
LICENSE.md
Normal file
|
@ -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.
|
|
@ -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.
|
||||
|
@ -34,12 +36,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.
|
||||
|
||||
- MAGITEK
|
||||
- DANCES
|
||||
- ESPERS
|
||||
- ITEM MAGIC
|
||||
- ITEM EFFECTS
|
||||
- COLOSSEUM
|
||||
- SHOPS
|
||||
- TREASURE CHESTS
|
||||
- JUNCTIONS
|
||||
|
|
271
main.py
271
main.py
|
@ -2,7 +2,7 @@
|
|||
|
||||
"""Parse BCEX (or BCCE) logs into json objects."""
|
||||
|
||||
__version__ = "0.4.1"
|
||||
__version__ = "0.5.1"
|
||||
__author__ = "Trysdyn Black"
|
||||
|
||||
import json
|
||||
|
@ -10,18 +10,11 @@ import sys
|
|||
from pathlib import Path
|
||||
|
||||
|
||||
class Parser:
|
||||
class Parser: # noqa: PLR0904
|
||||
"""
|
||||
BCEX/BCCE spoiler logfile parser.
|
||||
|
||||
Sections missing support:
|
||||
- MAGITEK
|
||||
- DANCES
|
||||
- ESPERS
|
||||
- ITEM MAGIC
|
||||
- ITEM EFFECTS
|
||||
- COLOSSEUM
|
||||
- SHOPS
|
||||
- TREASURE CHESTS
|
||||
- JUNCTIONS
|
||||
"""
|
||||
|
@ -50,6 +43,7 @@ class Parser:
|
|||
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("|"):
|
||||
|
@ -67,16 +61,30 @@ class Parser:
|
|||
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"):
|
||||
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:
|
||||
info["special"] = {}
|
||||
# Morph results, with a percent chance in each one
|
||||
elif line.startswith("MORPH"):
|
||||
_, chance, items = line.split(" ", 2)
|
||||
# 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}
|
||||
# Everything else is a simple k: v list where v is comma-delimited
|
||||
else:
|
||||
for k in ["immune", "auto", "skills", "steal", "drops", "location"]:
|
||||
|
@ -137,6 +145,7 @@ class Parser:
|
|||
# Name
|
||||
if line[0:2].isdigit():
|
||||
name = line[4:]
|
||||
info["name"] = name
|
||||
|
||||
# Stat chart rows: BCEX Version only
|
||||
elif line.startswith("|"):
|
||||
|
@ -365,6 +374,248 @@ 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] = {"name": 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": {}, "name": esper}
|
||||
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 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 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:
|
||||
"""
|
||||
|
|
Loading…
Add table
Reference in a new issue