Archipelago/worlds/terraria/Checks.py

736 lines
23 KiB
Python

from BaseClasses import Item, Location
from typing import Tuple, Union, Set, List, Dict
import string
import pkgutil
class TerrariaItem(Item):
game = "Terraria"
class TerrariaLocation(Location):
game = "Terraria"
def add_token(
tokens: List[Tuple[int, int, Union[str, int, None]]],
token: Union[str, int],
token_index: int,
):
if token == "":
return
if type(token) == str:
tokens.append((token_index, 0, token.rstrip()))
elif type(token) == int:
tokens.append((token_index, 1, token))
IDENT = 0
NUM = 1
SEMI = 2
HASH = 3
AT = 4
NOT = 5
AND = 6
OR = 7
LPAREN = 8
RPAREN = 9
END_OF_LINE = 10
CHAR_TO_TOKEN_ID = {
";": SEMI,
"#": HASH,
"@": AT,
"~": NOT,
"&": AND,
"|": OR,
"(": LPAREN,
")": RPAREN,
}
TOKEN_ID_TO_CHAR = {id: char for char, id in CHAR_TO_TOKEN_ID.items()}
def tokens(rule: str) -> List[Tuple[int, int, Union[str, int, None]]]:
token_list = []
token = ""
token_index = 0
escaped = False
for index, char in enumerate(rule):
if escaped:
if token == "":
token_index = index
token += char
escaped = False
elif char == "\\":
if type(token) == int:
add_token(token_list, token, token_index)
token = ""
escaped = True
elif char == "/" and token.endswith("/"):
add_token(token_list, token[:-1], token_index)
return token_list
elif token == "" and char.isspace():
pass
elif token == "" and char.isdigit():
token_index = index
token = int(char)
elif type(token) == int and char.isdigit():
token = token * 10 + int(char)
elif (id := CHAR_TO_TOKEN_ID.get(char)) != None:
add_token(token_list, token, token_index)
token_list.append((index, id, None))
token = ""
else:
if token == "":
token_index = index
token += char
add_token(token_list, token, token_index)
return token_list
NAME = 0
NAME_SEMI = 1
FLAG_OR_SEMI = 2
POST_FLAG = 3
FLAG = 4
FLAG_ARG = 5
FLAG_ARG_END = 6
COND_OR_SEMI = 7
POST_COND = 8
COND = 9
LOC = 10
FN = 11
POST_FN = 12
FN_ARG = 13
FN_ARG_END = 14
END = 15
GOAL = 16
POS_FMT = [
"name or `#`",
"`;`",
"flag or `;`",
"`;`, `|`, or `(`",
"flag",
"text or number",
"`)`",
"name, `#`, `@`, `~`, `(`, or `;`",
"`;`, `&`, `|`, or `)`",
"name, `#`, `@`, `~`, or `(`",
"name",
"name",
"`(`, `;`, `&`, `|`, or `)`",
"text or number",
"`)`",
"end of line",
"goal",
]
RWD_NAME = 0
RWD_NAME_SEMI = 1
RWD_FLAG = 2
RWD_FLAG_SEMI = 3
RWD_END = 4
RWD_LABEL = 5
RWD_POS_FMT = ["name or `#`", "`;`", "flag", "`;`", "end of line", "name"]
def unexpected(line: int, char: int, id: int, token, pos, pos_fmt, file):
if id == IDENT or id == NUM:
token_fmt = f"`{token}`"
elif id == END_OF_LINE:
token_fmt = "end of line"
else:
token_fmt = f"`{TOKEN_ID_TO_CHAR[id]}`"
raise Exception(
f"in `{file}`, found {token_fmt} at {line + 1}:{char + 1}; expected {pos_fmt[pos]}"
)
COND_ITEM = 0
COND_LOC = 1
COND_FN = 2
COND_GROUP = 3
def validate_conditions(
rule: str,
rule_indices: dict,
conditions: List[
Tuple[
bool, int, Union[str, Tuple[Union[bool, None], list]], Union[str, int, None]
]
],
):
for _, type, condition, _ in conditions:
if type == COND_ITEM:
if condition not in rule_indices:
raise Exception(f"item `{condition}` in `{rule}` is not defined")
elif type == COND_LOC:
if condition not in rule_indices:
raise Exception(f"location `{condition}` in `{rule}` is not defined")
elif type == COND_FN:
if condition not in {
"npc",
"calamity",
"grindy",
"pickaxe",
"hammer",
"mech_boss",
"minions",
}:
raise Exception(f"function `{condition}` in `{rule}` is not defined")
elif type == COND_GROUP:
_, conditions = condition
validate_conditions(rule, rule_indices, conditions)
def mark_progression(
conditions: List[
Tuple[
bool, int, Union[str, Tuple[Union[bool, None], list]], Union[str, int, None]
]
],
progression: Set[str],
rules: list,
rule_indices: dict,
loc_to_item: dict,
):
for _, type, condition, _ in conditions:
if type == COND_ITEM:
prog = condition in progression
progression.add(loc_to_item[condition])
_, flags, _, conditions = rules[rule_indices[condition]]
if (
not prog
and "Achievement" not in flags
and "Location" not in flags
and "Item" not in flags
):
mark_progression(
conditions, progression, rules, rule_indices, loc_to_item
)
elif type == COND_LOC:
_, _, _, conditions = rules[rule_indices[condition]]
mark_progression(conditions, progression, rules, rule_indices, loc_to_item)
elif type == COND_GROUP:
_, conditions = condition
mark_progression(conditions, progression, rules, rule_indices, loc_to_item)
def read_data() -> Tuple[
# Goal to rule index that ends that goal's range and the locations required
List[Tuple[int, Set[str]]],
# Rules
List[
Tuple[
# Rule
str,
# Flag to flag arg
Dict[str, Union[str, int, None]],
# True = or, False = and, None = N/A
Union[bool, None],
# Conditions
List[
Tuple[
# True = positive, False = negative
bool,
# Condition type
int,
# Condition name or list (True = or, False = and, None = N/A) (list shares type with outer)
Union[str, Tuple[Union[bool, None], List]],
# Condition arg
Union[str, int, None],
]
],
]
],
# Rule to rule index
Dict[str, int],
# Label to rewards
Dict[str, List[str]],
# Reward to flags
Dict[str, Set[str]],
# Item name to ID
Dict[str, int],
# Location name to ID
Dict[str, int],
# NPCs
List[str],
# Pickaxe to pick power
Dict[str, int],
# Hammer to hammer power
Dict[str, int],
# Mechanical bosses
List[str],
# Calamity final bosses
List[str],
# Progression rules
Set[str],
# Armor to minion count,
Dict[str, int],
# Accessory to minion count,
Dict[str, int],
]:
next_id = 0x7E0000
item_name_to_id = {}
goals = []
goal_indices = {}
rules = []
rule_indices = {}
loc_to_item = {}
npcs = []
pickaxes = {}
hammers = {}
mech_boss_loc = []
mech_bosses = []
final_boss_loc = []
final_bosses = []
armor_minions = {}
accessory_minions = {}
progression = set()
for line, rule in enumerate(
pkgutil.get_data(__name__, "Rules.dsv").decode().splitlines()
):
goal = None
name = None
flags = {}
sign = True
operator = None
outer = []
conditions = []
pos = NAME
for char, id, token in tokens(rule):
if pos == NAME:
if id == IDENT:
name = token
pos = NAME_SEMI
elif id == HASH:
pos = GOAL
else:
unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
elif pos == NAME_SEMI:
if id == SEMI:
pos = FLAG_OR_SEMI
else:
unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
elif pos == FLAG_OR_SEMI:
if id == IDENT:
flag = token
pos = POST_FLAG
elif id == SEMI:
pos = COND_OR_SEMI
else:
unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
elif pos == POST_FLAG:
if id == SEMI:
if flag is not None:
if flag in flags:
raise Exception(
f"set flag `{flag}` at {line + 1}:{char + 1} that was already set"
)
flags[flag] = None
flag = None
pos = COND_OR_SEMI
elif id == OR:
pos = FLAG
elif id == LPAREN:
pos = FLAG_ARG
else:
unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
elif pos == FLAG:
if id == IDENT:
if flag is not None:
if flag in flags:
raise Exception(
f"set flag `{flag}` at {line + 1}:{char + 1} that was already set"
)
flags[flag] = None
flag = None
flag = token
pos = POST_FLAG
else:
unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
elif pos == FLAG_ARG:
if id == IDENT or id == NUM:
if flag in flags:
raise Exception(
f"set flag `{flag}` at {line + 1}:{char + 1} that was already set"
)
flags[flag] = token
flag = None
pos = FLAG_ARG_END
else:
unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
elif pos == FLAG_ARG_END:
if id == RPAREN:
pos = POST_FLAG
else:
unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
elif pos == COND_OR_SEMI:
if id == IDENT:
conditions.append((sign, COND_ITEM, token, None))
sign = True
pos = POST_COND
elif id == HASH:
pos = LOC
elif id == AT:
pos = FN
elif id == NOT:
sign = not sign
pos = COND
elif id == LPAREN:
outer.append((sign, None, conditions))
conditions = []
sign = True
pos = COND
else:
unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
elif pos == POST_COND:
if id == SEMI:
if outer:
raise Exception(
f"found `;` at {line + 1}:{char + 1} after unclosed `(`"
)
pos = END
elif id == AND:
if operator is True:
raise Exception(
f"found `&` at {line + 1}:{char + 1} in group containing `|`"
)
operator = False
pos = COND
elif id == OR:
if operator is False:
raise Exception(
f"found `|` at {line + 1}:{char + 1} in group containing `&`"
)
operator = True
pos = COND
elif id == RPAREN:
if not outer:
raise Exception(
f"found `)` at {line + 1}:{char + 1} without matching `(`"
)
condition = operator, conditions
sign, operator, conditions = outer.pop()
conditions.append((sign, COND_GROUP, condition, None))
sign = True
pos = POST_COND
else:
unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
elif pos == COND:
if id == IDENT:
conditions.append((sign, COND_ITEM, token, None))
sign = True
pos = POST_COND
elif id == HASH:
pos = LOC
elif id == AT:
pos = FN
elif id == NOT:
sign = not sign
elif id == LPAREN:
outer.append((sign, operator, conditions))
conditions = []
sign = True
operator = None
else:
unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
elif pos == LOC:
if id == IDENT:
conditions.append((sign, COND_LOC, token, None))
sign = True
pos = POST_COND
else:
unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
elif pos == FN:
if id == IDENT:
function = token
pos = POST_FN
else:
unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
elif pos == POST_FN:
if id == LPAREN:
pos = FN_ARG
elif id == SEMI:
conditions.append((sign, COND_FN, function, None))
pos = END
elif id == AND:
conditions.append((sign, COND_FN, function, None))
sign = True
if operator is True:
raise Exception(
f"found `&` at {line + 1}:{char + 1} in group containing `|`"
)
operator = False
pos = COND
elif id == OR:
conditions.append((sign, COND_FN, function, None))
sign = True
if operator is False:
raise Exception(
f"found `|` at {line + 1}:{char + 1} in group containing `&`"
)
operator = True
pos = COND
elif id == RPAREN:
conditions.append((sign, COND_FN, function, None))
if not outer:
raise Exception(
f"found `)` at {line + 1}:{char + 1} without matching `(`"
)
condition = operator, conditions
sign, operator, conditions = outer.pop()
conditions.append((sign, COND_GROUP, condition, None))
sign = True
pos = POST_COND
else:
unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
elif pos == FN_ARG:
if id == IDENT or id == NUM:
conditions.append((sign, COND_FN, function, token))
sign = True
pos = FN_ARG_END
else:
unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
elif pos == FN_ARG_END:
if id == RPAREN:
pos = POST_COND
else:
unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
elif pos == END:
unexpected(line, char, id, token, pos)
elif pos == GOAL:
if id == IDENT:
goal = token
pos = END
else:
unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
if pos != NAME and pos != FLAG_OR_SEMI and pos != COND_OR_SEMI and pos != END:
unexpected(line, char + 1, END_OF_LINE, None, pos, POS_FMT, "Rules.dsv")
if name:
if name in rule_indices:
raise Exception(
f"rule `{name}` on line `{line + 1}` shadows a previous rule"
)
rule_indices[name] = len(rules)
rules.append((name, flags, operator, conditions))
if "Item" in flags:
item_name = flags["Item"] or f"Post-{name}"
if item_name in item_name_to_id:
raise Exception(
f"item `{item_name}` on line `{line + 1}` shadows a previous item"
)
item_name_to_id[item_name] = next_id
next_id += 1
loc_to_item[name] = item_name
else:
loc_to_item[name] = name
if "Npc" in flags:
npcs.append(name)
if (power := flags.get("Pickaxe")) is not None:
pickaxes[name] = power
if (power := flags.get("Hammer")) is not None:
hammers[name] = power
if "Mech Boss" in flags:
mech_bosses.append(flags["Item"] or f"Post-{name}")
mech_boss_loc.append(name)
if "Final Boss" in flags:
final_bosses.append(flags["Item"] or f"Post-{name}")
final_boss_loc.append(name)
if (minions := flags.get("ArmorMinions")) is not None:
armor_minions[name] = minions
if (minions := flags.get("Minions")) is not None:
accessory_minions[name] = minions
if goal:
if goal in goal_indices:
raise Exception(
f"goal `{goal}` on line `{line + 1}` shadows a previous goal"
)
goal_indices[goal] = len(goals)
goals.append((len(rules), set()))
for name, flags, _, _ in rules:
if "Goal" in flags:
_, items = goals[
goal_indices[
name.translate(str.maketrans("", "", string.punctuation))
.replace(" ", "_")
.lower()
]
]
items.add(name)
_, mech_boss_items = goals[goal_indices["mechanical_bosses"]]
mech_boss_items.update(mech_boss_loc)
_, final_boss_items = goals[goal_indices["calamity_final_bosses"]]
final_boss_items.update(final_boss_loc)
for name, _, _, conditions in rules:
validate_conditions(name, rule_indices, conditions)
for name, flags, _, conditions in rules:
prog = False
if (
"Npc" in flags
or "Goal" in flags
or "Pickaxe" in flags
or "Hammer" in flags
or "Mech Boss" in flags
or "Minions" in flags
or "ArmorMinions" in flags
):
progression.add(loc_to_item[name])
prog = True
if prog or "Location" in flags or "Achievement" in flags:
mark_progression(conditions, progression, rules, rule_indices, loc_to_item)
# Will be randomized via `slot_randoms` / `self.multiworld.random`
label = None
labels = {}
rewards = {}
for line in pkgutil.get_data(__name__, "Rewards.dsv").decode().splitlines():
reward = None
flags = set()
pos = RWD_NAME
for char, id, token in tokens(line):
if pos == RWD_NAME:
if id == IDENT:
reward = f"Reward: {token}"
pos = RWD_NAME_SEMI
elif id == HASH:
pos = RWD_LABEL
else:
unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv")
elif pos == RWD_NAME_SEMI:
if id == SEMI:
pos = RWD_FLAG
else:
unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv")
elif pos == RWD_FLAG:
if id == IDENT:
if token in flags:
raise Exception(
f"set flag `{token}` at {line + 1}:{char + 1} that was already set"
)
flags.add(token)
pos = RWD_FLAG_SEMI
else:
unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv")
elif pos == RWD_FLAG_SEMI:
if id == SEMI:
pos = RWD_END
else:
unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv")
elif pos == RWD_END:
unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv")
elif pos == RWD_LABEL:
if id == IDENT:
label = token
if label in labels:
raise Exception(
f"started label `{label}` at {line + 1}:{char + 1} that was already used"
)
labels[label] = []
pos = RWD_END
else:
unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv")
if pos != RWD_NAME and pos != RWD_FLAG and pos != RWD_END:
unexpected(line, char + 1, END_OF_LINE, None, pos)
if reward:
if reward in rewards:
raise Exception(
f"reward `{reward}` on line `{line + 1}` shadows a previous reward"
)
rewards[reward] = flags
if not label:
raise Exception(
f"reward `{reward}` on line `{line + 1}` is not labeled"
)
labels[label].append(reward)
if reward in item_name_to_id:
raise Exception(
f"item `{reward}` on line `{line + 1}` shadows a previous item"
)
item_name_to_id[reward] = next_id
next_id += 1
item_name_to_id["Reward: Coins"] = next_id
item_name_to_id["Victory"] = next_id + 1
next_id += 2
location_name_to_id = {}
for name, flags, _, _ in rules:
if "Location" in flags or "Achievement" in flags:
if name in location_name_to_id:
raise Exception(f"location `{name}` shadows a previous location")
location_name_to_id[name] = next_id
next_id += 1
return (
goals,
rules,
rule_indices,
labels,
rewards,
item_name_to_id,
location_name_to_id,
npcs,
pickaxes,
hammers,
mech_bosses,
final_bosses,
progression,
armor_minions,
accessory_minions,
)
(
goals,
rules,
rule_indices,
labels,
rewards,
item_name_to_id,
location_name_to_id,
npcs,
pickaxes,
hammers,
mech_bosses,
final_bosses,
progression,
armor_minions,
accessory_minions,
) = read_data()