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()