This is a combined PR for assorted Hollow Knight updates for June 2022 that have cleared testing. It supersedes any HK-exclusive PRs open by myself or @Alchav unless stated otherwise. Summary of changes below: * Implement Split Claw, Split Cloak, Split Superdash, Randomize Nail, Randomize Focus, Randomize Swim and Elevator * Pass options (@Alchav) * Add support for Deathlink with three different modes (@dewiniaid) * Add customizable additional shop slots per-shop (@Alchav) and overall (@dewiniaid) * Overhaul shop cost output to be more generic and account for all locations with standard costs (such as Stag Stations, Cornifer, and Divine) (@dewiniaid) * Add "CostSanity", allowing random prices using any cost type to be chosen for any location with a cost. (e.g. a Stag station requiring 15 grubs to obtain an item) * Item classification fixes (Map and Journal items are fillter, Mask Shards/Pale Ore/Vessel Fragments are useful) (@Alchav) * Fix Ijii -> Jiji (@Alchav ) * General code quality updates The above changes are only for the HK world.
		
			
				
	
	
		
			469 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			469 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
"""
 | 
						|
Logic Extractor designed for "Randomizer 4".
 | 
						|
Place a Randomizer 4 compatible "Resources" folder next to this script, then run the script, to create AP data.
 | 
						|
"""
 | 
						|
import os
 | 
						|
import json
 | 
						|
import typing
 | 
						|
import ast
 | 
						|
 | 
						|
import jinja2
 | 
						|
 | 
						|
try:
 | 
						|
    from ast import unparse
 | 
						|
except ImportError:
 | 
						|
    # Py 3.8 and earlier compatibility module
 | 
						|
    from astunparse import unparse
 | 
						|
 | 
						|
from Utils import get_text_between
 | 
						|
 | 
						|
 | 
						|
def put_digits_at_end(text: str) -> str:
 | 
						|
    for x in range(len(text)):
 | 
						|
        if text[0].isdigit():
 | 
						|
            text = text[1:] + text[0]
 | 
						|
        else:
 | 
						|
            break
 | 
						|
    return text
 | 
						|
 | 
						|
 | 
						|
def hk_loads(file: str) -> typing.Any:
 | 
						|
    with open(file, encoding="utf-8-sig") as f:
 | 
						|
        data = f.read()
 | 
						|
    new_data = []
 | 
						|
    for row in data.split("\n"):
 | 
						|
        if not row.strip().startswith(r"//"):
 | 
						|
            new_data.append(row)
 | 
						|
    return json.loads("\n".join(new_data))
 | 
						|
 | 
						|
 | 
						|
def hk_convert(text: str) -> str:
 | 
						|
    parts = text.replace("(", "( ").replace(")", " )").replace(">", " > ").replace("=", "==").split()
 | 
						|
    new_parts = []
 | 
						|
    for part in parts:
 | 
						|
        part = put_digits_at_end(part)
 | 
						|
 | 
						|
        if part in items or part in effect_names or part in event_names or part in connectors:
 | 
						|
            new_parts.append(f"\"{part}\"")
 | 
						|
        else:
 | 
						|
            new_parts.append(part)
 | 
						|
    text = " ".join(new_parts)
 | 
						|
    result = ""
 | 
						|
    parts = text.split("$StartLocation[")
 | 
						|
    for i, part in enumerate(parts[:-1]):
 | 
						|
        result += part + "StartLocation[\""
 | 
						|
        parts[i+1] = parts[i+1].replace("]", "\"]", 1)
 | 
						|
 | 
						|
    text = result + parts[-1]
 | 
						|
 | 
						|
    result = ""
 | 
						|
    parts = text.split("COMBAT[")
 | 
						|
    for i, part in enumerate(parts[:-1]):
 | 
						|
        result += part + "COMBAT[\""
 | 
						|
        parts[i+1] = parts[i+1].replace("]", "\"]", 1)
 | 
						|
 | 
						|
    text = result + parts[-1]
 | 
						|
    return text.replace("+", "and").replace("|", "or").replace("$", "").strip()
 | 
						|
 | 
						|
 | 
						|
class Absorber(ast.NodeTransformer):
 | 
						|
    additional_truths = set()
 | 
						|
    additional_falses = set()
 | 
						|
 | 
						|
    def __init__(self, truth_values, false_values):
 | 
						|
        self.truth_values = truth_values
 | 
						|
        self.truth_values |= {"True", "None", "ANY", "ITEMRANDO"}
 | 
						|
        self.false_values = false_values
 | 
						|
        self.false_values |= {"False", "NONE"}
 | 
						|
 | 
						|
        super(Absorber, self).__init__()
 | 
						|
 | 
						|
    def generic_visit(self, node: ast.AST) -> ast.AST:
 | 
						|
        # Need to call super() in any case to visit child nodes of the current one.
 | 
						|
        node = super().generic_visit(node)
 | 
						|
        return node
 | 
						|
 | 
						|
    def visit_BoolOp(self, node: ast.BoolOp) -> ast.AST:
 | 
						|
        if type(node.op) == ast.And:
 | 
						|
            if self.is_always_true(node.values[0]):
 | 
						|
                return self.visit(node.values[1])
 | 
						|
            if self.is_always_true(node.values[1]):
 | 
						|
                return self.visit(node.values[0])
 | 
						|
            if self.is_always_false(node.values[0]) or self.is_always_false(node.values[1]):
 | 
						|
                return ast.Constant(False, ctx=ast.Load())
 | 
						|
        elif type(node.op) == ast.Or:
 | 
						|
            if self.is_always_true(node.values[0]) or self.is_always_true(node.values[1]):
 | 
						|
                return ast.Constant(True, ctx=ast.Load())
 | 
						|
            if self.is_always_false(node.values[0]):
 | 
						|
                return self.visit(node.values[1])
 | 
						|
            if self.is_always_false(node.values[1]):
 | 
						|
                return self.visit(node.values[0])
 | 
						|
        return self.generic_visit(node)
 | 
						|
 | 
						|
    def visit_Name(self, node: ast.Name) -> ast.AST:
 | 
						|
        if node.id in self.truth_values:
 | 
						|
            return ast.Constant(True, ctx=node.ctx)
 | 
						|
        if node.id in self.false_values:
 | 
						|
            return ast.Constant(False, ctx=node.ctx)
 | 
						|
        if node.id in logic_options:
 | 
						|
            return ast.Call(
 | 
						|
                func=ast.Attribute(value=ast.Name(id='state', ctx=ast.Load()), attr='_hk_option', ctx=ast.Load()),
 | 
						|
                args=[ast.Name(id="player", ctx=ast.Load()), ast.Constant(value=logic_options[node.id])], keywords=[])
 | 
						|
        if node.id in macros:
 | 
						|
            return macros[node.id].body
 | 
						|
        if node.id in region_names:
 | 
						|
            raise Exception(f"Should be event {node.id}")
 | 
						|
        # You'd think this means reach Scene/Region of that name, but is actually waypoint/event
 | 
						|
        # if node.id in region_names:
 | 
						|
        #     return ast.Call(
 | 
						|
        #         func=ast.Attribute(value=ast.Name(id='state', ctx=ast.Load()), attr='can_reach', ctx=ast.Load()),
 | 
						|
        #         args=[ast.Constant(value=node.id),
 | 
						|
        #               ast.Constant(value="Region"),
 | 
						|
        #               ast.Name(id="player", ctx=ast.Load())],
 | 
						|
        #         keywords=[])
 | 
						|
        return self.generic_visit(node)
 | 
						|
 | 
						|
    def visit_Constant(self, node: ast.Constant) -> ast.AST:
 | 
						|
        if type(node.value) == str:
 | 
						|
            logic_items.add(node.value)
 | 
						|
            return ast.Call(
 | 
						|
                func=ast.Attribute(value=ast.Name(id='state', ctx=ast.Load()), attr='count', ctx=ast.Load()),
 | 
						|
                args=[ast.Constant(value=node.value), ast.Name(id="player", ctx=ast.Load())], keywords=[])
 | 
						|
 | 
						|
        return node
 | 
						|
 | 
						|
    def visit_Subscript(self, node: ast.Subscript) -> ast.AST:
 | 
						|
        if node.value.id == "NotchCost":
 | 
						|
            notches = [ast.Constant(value=notch.value - 1) for notch in node.slice.elts]  # apparently 1-indexed
 | 
						|
            return ast.Call(
 | 
						|
                func=ast.Attribute(value=ast.Name(id='state', ctx=ast.Load()), attr='_hk_notches', ctx=ast.Load()),
 | 
						|
                args=[ast.Name(id="player", ctx=ast.Load())] + notches, keywords=[])
 | 
						|
        elif node.value.id == "StartLocation":
 | 
						|
            node.slice.value = node.slice.value.replace(" ", "_").lower()
 | 
						|
            if node.slice.value in removed_starts:
 | 
						|
                return ast.Constant(False, ctx=node.ctx)
 | 
						|
            return ast.Call(
 | 
						|
                func=ast.Attribute(value=ast.Name(id='state', ctx=ast.Load()), attr='_hk_start', ctx=ast.Load()),
 | 
						|
                args=[ast.Name(id="player", ctx=ast.Load()), node.slice], keywords=[])
 | 
						|
        elif node.value.id == "COMBAT":
 | 
						|
            return macros[unparse(node)].body
 | 
						|
        else:
 | 
						|
            name = unparse(node)
 | 
						|
            if name in self.additional_truths:
 | 
						|
                return ast.Constant(True, ctx=ast.Load())
 | 
						|
            elif name in self.additional_falses:
 | 
						|
                return ast.Constant(False, ctx=ast.Load())
 | 
						|
            elif name in macros:
 | 
						|
                # macro such as "COMBAT[White_Palace_Arenas]"
 | 
						|
                return macros[name].body
 | 
						|
            else:
 | 
						|
                # assume Entrance
 | 
						|
                entrance = unparse(node)
 | 
						|
                assert entrance in connectors, entrance
 | 
						|
                return ast.Call(
 | 
						|
                    func=ast.Attribute(value=ast.Name(id='state', ctx=ast.Load()), attr='can_reach', ctx=ast.Load()),
 | 
						|
                    args=[ast.Constant(value=entrance),
 | 
						|
                          ast.Constant(value="Entrance"),
 | 
						|
                          ast.Name(id="player", ctx=ast.Load())],
 | 
						|
                    keywords=[])
 | 
						|
        return node
 | 
						|
 | 
						|
    def is_always_true(self, node):
 | 
						|
        if isinstance(node, ast.Name) and (node.id in self.truth_values or node.id in self.additional_truths):
 | 
						|
            return True
 | 
						|
        if isinstance(node, ast.Subscript) and unparse(node) in self.additional_truths:
 | 
						|
            return True
 | 
						|
 | 
						|
    def is_always_false(self, node):
 | 
						|
        if isinstance(node, ast.Name) and (node.id in self.false_values or node.id in self.additional_falses):
 | 
						|
            return True
 | 
						|
        if isinstance(node, ast.Subscript) and unparse(node) in self.additional_falses:
 | 
						|
            return True
 | 
						|
 | 
						|
 | 
						|
def get_parser(truths: typing.Set[str] = frozenset(), falses: typing.Set[str] = frozenset()):
 | 
						|
    return Absorber(truths, falses)
 | 
						|
 | 
						|
 | 
						|
def ast_parse(parser, rule_text, truths: typing.Set[str] = frozenset(), falses: typing.Set[str] = frozenset()):
 | 
						|
    tree = ast.parse(hk_convert(rule_text), mode='eval')
 | 
						|
    parser.additional_truths = truths
 | 
						|
    parser.additional_falses = falses
 | 
						|
    new_tree = parser.visit(tree)
 | 
						|
    parser.additional_truths = set()
 | 
						|
    parser.additional_truths = set()
 | 
						|
    return new_tree
 | 
						|
 | 
						|
 | 
						|
world_folder = os.path.dirname(__file__)
 | 
						|
 | 
						|
resources_source = os.path.join(world_folder, "Resources")
 | 
						|
data_folder = os.path.join(resources_source, "Data")
 | 
						|
logic_folder = os.path.join(resources_source, "Logic")
 | 
						|
logic_options: typing.Dict[str, str] = hk_loads(os.path.join(data_folder, "logic_settings.json"))
 | 
						|
for logic_key, logic_value in logic_options.items():
 | 
						|
    logic_options[logic_key] = logic_value.split(".", 1)[-1]
 | 
						|
 | 
						|
vanilla_cost_data: typing.Dict[str, typing.Dict[str, typing.Any]] = hk_loads(os.path.join(data_folder, "costs.json"))
 | 
						|
vanilla_location_costs = {
 | 
						|
    key: {
 | 
						|
        value["term"]: int(value["amount"])
 | 
						|
    }
 | 
						|
    for key, value in vanilla_cost_data.items()
 | 
						|
    if value["amount"] > 0 and value["term"] == "GEO"
 | 
						|
}
 | 
						|
 | 
						|
salubra_geo_costs_by_charm_count = {
 | 
						|
    5: 120,
 | 
						|
    10: 500,
 | 
						|
    18: 900,
 | 
						|
    25: 1400,
 | 
						|
    40: 800
 | 
						|
}
 | 
						|
 | 
						|
# Can't extract this data, so supply it ourselves.  Source: the wiki
 | 
						|
vanilla_shop_costs = {
 | 
						|
    ('Sly', 'Simple_Key'): [{'GEO': 950}],
 | 
						|
    ('Sly', 'Rancid_Egg'): [{'GEO': 60}],
 | 
						|
    ('Sly', 'Lumafly_Lantern'): [{'GEO': 1800}],
 | 
						|
    ('Sly', 'Gathering_Swarm'): [{'GEO': 300}],
 | 
						|
    ('Sly', 'Stalwart_Shell'): [{'GEO': 200}],
 | 
						|
    ('Sly', 'Mask_Shard'): [
 | 
						|
        {'GEO': 150},
 | 
						|
        {'GEO': 500},
 | 
						|
    ],
 | 
						|
    ('Sly', 'Vessel_Fragment'): [{'GEO': 550}],
 | 
						|
    ('Sly_(Key)', 'Heavy_Blow'): [{'GEO': 350}],
 | 
						|
    ('Sly_(Key)', 'Elegant_Key'): [{'GEO': 800}],
 | 
						|
    ('Sly_(Key)', 'Mask_Shard'): [
 | 
						|
        {'GEO': 800},
 | 
						|
        {'GEO': 1500},
 | 
						|
    ],
 | 
						|
    ('Sly_(Key)', 'Vessel_Fragment'): [{'GEO': 900}],
 | 
						|
    ('Sly_(Key)', 'Sprintmaster'): [{'GEO': 400}],
 | 
						|
 | 
						|
    ('Iselda', 'Wayward_Compass'): [{'GEO': 220}],
 | 
						|
    ('Iselda', 'Quill'): [{'GEO': 120}],
 | 
						|
 | 
						|
    ('Salubra', 'Lifeblood_Heart'): [{'GEO': 250}],
 | 
						|
    ('Salubra', 'Longnail'): [{'GEO': 300}],
 | 
						|
    ('Salubra', 'Steady_Body'): [{'GEO': 120}],
 | 
						|
    ('Salubra', 'Shaman_Stone'): [{'GEO': 220}],
 | 
						|
    ('Salubra', 'Quick_Focus'): [{'GEO': 800}],
 | 
						|
 | 
						|
    ('Leg_Eater', 'Fragile_Heart'): [{'GEO': 350}],
 | 
						|
    ('Leg_Eater', 'Fragile_Greed'): [{'GEO': 250}],
 | 
						|
    ('Leg_Eater', 'Fragile_Strength'): [{'GEO': 600}],
 | 
						|
}
 | 
						|
extra_pool_options: typing.List[typing.Dict[str, typing.Any]] = hk_loads(os.path.join(data_folder, "pools.json"))
 | 
						|
pool_options: typing.Dict[str, typing.Tuple[typing.List[str], typing.List[str]]] = {}
 | 
						|
for option in extra_pool_options:
 | 
						|
    if option["Path"] != "False":
 | 
						|
        items: typing.List[str] = []
 | 
						|
        locations: typing.List[str] = []
 | 
						|
        for pairing in option["Vanilla"]:
 | 
						|
            items.append(pairing["item"])
 | 
						|
            location_name = pairing["location"]
 | 
						|
            item_costs = pairing.get("costs", [])
 | 
						|
            if item_costs:
 | 
						|
                if any(cost_entry["term"] == "CHARMS" for cost_entry in item_costs):
 | 
						|
                    location_name += "_(Requires_Charms)"
 | 
						|
                #vanilla_shop_costs[pairing["location"], pairing["item"]] = \
 | 
						|
                cost = {
 | 
						|
                    entry["term"]: int(entry["amount"]) for entry in item_costs
 | 
						|
                }
 | 
						|
                # Rando4 doesn't include vanilla geo costs for Salubra charms, so dirty hardcode here.
 | 
						|
                if 'CHARMS' in cost:
 | 
						|
                    geo = salubra_geo_costs_by_charm_count.get(cost['CHARMS'])
 | 
						|
                    if geo:
 | 
						|
                        cost['GEO'] = geo
 | 
						|
 | 
						|
                key = (pairing["location"], pairing["item"])
 | 
						|
                vanilla_shop_costs.setdefault(key, []).append(cost)
 | 
						|
 | 
						|
            locations.append(location_name)
 | 
						|
        if option["Path"]:
 | 
						|
            # basename carries over from prior entry if no Path given
 | 
						|
            basename = option["Path"].split(".", 1)[-1]
 | 
						|
        if not basename.startswith("Randomize"):
 | 
						|
            basename = "Randomize" + basename
 | 
						|
        assert len(items) == len(locations)
 | 
						|
        if items:  # skip empty pools
 | 
						|
            if basename in pool_options:
 | 
						|
                pool_options[basename] = pool_options[basename][0]+items, pool_options[basename][1]+locations
 | 
						|
            else:
 | 
						|
                pool_options[basename] = items, locations
 | 
						|
del extra_pool_options
 | 
						|
 | 
						|
# reverse all the vanilla shop costs (really, this is just for Salubra).
 | 
						|
# When we use these later, we pop off the end of the list so this ensures they are still sorted.
 | 
						|
vanilla_shop_costs = {
 | 
						|
    k: list(reversed(v)) for k, v in vanilla_shop_costs.items()
 | 
						|
}
 | 
						|
 | 
						|
# items
 | 
						|
items: typing.Dict[str, typing.Dict] = hk_loads(os.path.join(data_folder, "items.json"))
 | 
						|
logic_items: typing.Set[str] = set()
 | 
						|
for item_name in sorted(items):
 | 
						|
    item = items[item_name]
 | 
						|
    items[item_name] = item["Pool"]
 | 
						|
items: typing.Dict[str, str]
 | 
						|
 | 
						|
extra_item_data: typing.List[typing.Dict[str, typing.Any]] = hk_loads(os.path.join(logic_folder, "items.json"))
 | 
						|
item_effects: typing.Dict[str, typing.Dict[str, int]] = {}
 | 
						|
effect_names: typing.Set[str] = set()
 | 
						|
for item_data in extra_item_data:
 | 
						|
    if "FalseItem" in item_data:
 | 
						|
        item_data = item_data["FalseItem"]
 | 
						|
    effects = []
 | 
						|
    if "Effect" in item_data:
 | 
						|
        effects = [item_data["Effect"]]
 | 
						|
    elif "Effects" in item_data:
 | 
						|
        effects = item_data["Effects"]
 | 
						|
    for effect in effects:
 | 
						|
        effect_names.add(effect["Term"])
 | 
						|
    effects = {effect["Term"]: effect["Value"] for effect in effects if
 | 
						|
               effect["Term"] != item_data["Name"] and effect["Term"] not in {"GEO",
 | 
						|
                                                                              "HALLOWNESTSEALS",
 | 
						|
                                                                              "WANDERERSJOURNALS",
 | 
						|
                                                                              'HALLOWNESTSEALS',
 | 
						|
                                                                              "KINGSIDOLS",
 | 
						|
                                                                              'ARCANEEGGS',
 | 
						|
                                                                              'MAPS'
 | 
						|
                                                                              }}
 | 
						|
 | 
						|
    if effects:
 | 
						|
        item_effects[item_data["Name"]] = effects
 | 
						|
 | 
						|
del extra_item_data
 | 
						|
 | 
						|
# locations
 | 
						|
original_locations: typing.Dict[str, typing.Dict[str, typing.Any]] = hk_loads(os.path.join(data_folder, "locations.json"))
 | 
						|
del(original_locations["Start"])  # Starting Inventory works different in AP
 | 
						|
 | 
						|
locations: typing.List[str] = []
 | 
						|
locations_in_regions: typing.Dict[str, typing.List[str]] = {}
 | 
						|
location_to_region_lookup: typing.Dict[str, str] = {}
 | 
						|
multi_locations: typing.Dict[str, typing.List[str]] = {}
 | 
						|
for location_name, location_data in original_locations.items():
 | 
						|
    region_name = location_data["SceneName"]
 | 
						|
    if location_data["FlexibleCount"]:
 | 
						|
        location_names = [f"{location_name}_{count}" for count in range(1, 17)]
 | 
						|
        multi_locations[location_name] = location_names
 | 
						|
    else:
 | 
						|
        location_names = [location_name]
 | 
						|
 | 
						|
    location_to_region_lookup.update({name: region_name for name in location_names})
 | 
						|
    locations_in_regions.setdefault(region_name, []).extend(location_names)
 | 
						|
    locations.extend(location_names)
 | 
						|
del original_locations
 | 
						|
 | 
						|
# regions
 | 
						|
region_names: typing.Set[str] = set(hk_loads(os.path.join(data_folder, "rooms.json")))
 | 
						|
connectors_data: typing.Dict[str, typing.Dict[str, typing.Any]] = hk_loads(os.path.join(data_folder, "transitions.json"))
 | 
						|
connectors_logic: typing.List[typing.Dict[str, typing.Any]] = hk_loads(os.path.join(logic_folder, "transitions.json"))
 | 
						|
exits: typing.Dict[str, typing.List[str]] = {}
 | 
						|
connectors: typing.Dict[str, str] = {}
 | 
						|
one_ways: typing.Set[str] = set()
 | 
						|
for connector_name, connector_data in connectors_data.items():
 | 
						|
    exits.setdefault(connector_data["SceneName"], []).append(connector_name)
 | 
						|
    connectors[connector_name] = connector_data["VanillaTarget"]
 | 
						|
    if connector_data["Sides"] != "Both":
 | 
						|
        one_ways.add(connector_name)
 | 
						|
del connectors_data
 | 
						|
 | 
						|
# starts
 | 
						|
starts: typing.Dict[str, typing.Dict[str, typing.Any]] = hk_loads(os.path.join(data_folder, "starts.json"))
 | 
						|
 | 
						|
# only allow always valid starts for now
 | 
						|
removed_starts: typing.Set[str] = {name.replace(" ", "_").lower() for name, data in starts.items() if
 | 
						|
                                   name != "King's Pass"}
 | 
						|
 | 
						|
starts: typing.Dict[str, str] = {
 | 
						|
    name.replace(" ", "_").lower(): data["sceneName"] for name, data in starts.items() if name == "King's Pass"}
 | 
						|
 | 
						|
# logic
 | 
						|
falses = {"MAPAREARANDO", "FULLAREARANDO"}
 | 
						|
macros: typing.Dict[str, ast.AST] = {
 | 
						|
}
 | 
						|
parser = get_parser(set(), falses)
 | 
						|
extra_macros: typing.Dict[str, str] = hk_loads(os.path.join(logic_folder, "macros.json"))
 | 
						|
raw_location_rules: typing.List[typing.Dict[str, str]] = hk_loads(os.path.join(logic_folder, "locations.json"))
 | 
						|
events: typing.List[typing.Dict[str, typing.Any]] = hk_loads(os.path.join(logic_folder, "waypoints.json"))
 | 
						|
 | 
						|
event_names: typing.Set[str] = {event["name"] for event in events}
 | 
						|
 | 
						|
for macro_name, rule in extra_macros.items():
 | 
						|
    if macro_name not in macros:
 | 
						|
        macro_name = put_digits_at_end(macro_name)
 | 
						|
        if macro_name in items or macro_name in effect_names:
 | 
						|
            continue
 | 
						|
        assert macro_name not in events
 | 
						|
        rule = ast_parse(parser, rule)
 | 
						|
        macros[macro_name] = rule
 | 
						|
        if macro_name.startswith("COMBAT["):
 | 
						|
            name = get_text_between(macro_name, "COMBAT[", "]")
 | 
						|
            if not "'" in name:
 | 
						|
                macros[f"COMBAT['{name}']"] = rule
 | 
						|
            macros[f'COMBAT["{name}"]'] = rule
 | 
						|
 | 
						|
location_rules: typing.Dict[str, str] = {}
 | 
						|
for loc_obj in raw_location_rules:
 | 
						|
    loc_name = loc_obj["name"]
 | 
						|
    rule = loc_obj["logic"]
 | 
						|
    if rule != "ANY":
 | 
						|
        rule = ast_parse(parser, rule)
 | 
						|
        location_rules[loc_name] = unparse(rule)
 | 
						|
location_rules["Salubra_(Requires_Charms)"] = location_rules["Salubra"]
 | 
						|
 | 
						|
connectors_rules: typing.Dict[str, str] = {}
 | 
						|
for connector_obj in connectors_logic:
 | 
						|
    name = connector_obj["Name"]
 | 
						|
    rule = connector_obj["logic"]
 | 
						|
    rule = ast_parse(parser, rule)
 | 
						|
    rule = unparse(rule)
 | 
						|
    if rule != "True":
 | 
						|
        connectors_rules[name] = rule
 | 
						|
 | 
						|
event_rules: typing.Dict[str, str] = {}
 | 
						|
for event in events:
 | 
						|
    rule = ast_parse(parser, event["logic"])
 | 
						|
    rule = unparse(rule)
 | 
						|
    if rule != "True":
 | 
						|
        event_rules[event["name"]] = rule
 | 
						|
 | 
						|
 | 
						|
event_rules.update(connectors_rules)
 | 
						|
connectors_rules = {}
 | 
						|
 | 
						|
 | 
						|
# Apply some final fixes
 | 
						|
item_effects.update({
 | 
						|
    'Left_Mothwing_Cloak': {'LEFTDASH': 1},
 | 
						|
    'Right_Mothwing_Cloak': {'RIGHTDASH': 1},
 | 
						|
})
 | 
						|
names = sorted({"logic_options", "starts", "pool_options", "locations", "multi_locations", "location_to_region_lookup",
 | 
						|
                "event_names", "item_effects", "items", "logic_items", "region_names",
 | 
						|
                "exits", "connectors", "one_ways", "vanilla_shop_costs", "vanilla_location_costs"})
 | 
						|
warning = "# This module is written by Extractor.py, do not edit manually!.\n\n"
 | 
						|
with open(os.path.join(os.path.dirname(__file__), "ExtractedData.py"), "wt") as py:
 | 
						|
    py.write(warning)
 | 
						|
    for name in names:
 | 
						|
        var = globals()[name]
 | 
						|
        if type(var) == set:
 | 
						|
            # sort so a regen doesn't cause a file change every time
 | 
						|
            var = sorted(var)
 | 
						|
            var = "{"+str(var)[1:-1]+"}"
 | 
						|
        py.write(f"{name} = {var}\n")
 | 
						|
 | 
						|
 | 
						|
template_env: jinja2.Environment = \
 | 
						|
    jinja2.Environment(loader=jinja2.FileSystemLoader([os.path.join(os.path.dirname(__file__), "templates")]))
 | 
						|
rules_template = template_env.get_template("RulesTemplate.pyt")
 | 
						|
rules = rules_template.render(location_rules=location_rules, one_ways=one_ways, connectors_rules=connectors_rules,
 | 
						|
                              event_rules=event_rules)
 | 
						|
 | 
						|
with open("GeneratedRules.py", "wt") as py:
 | 
						|
    py.write(warning)
 | 
						|
    py.write(rules)
 |