508 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			508 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | import ast | ||
|  | from collections import defaultdict | ||
|  | from inspect import signature, _ParameterKind | ||
|  | import logging | ||
|  | import re | ||
|  | 
 | ||
|  | from .Items import item_table | ||
|  | from .Location import OOTLocation | ||
|  | from .Regions import TimeOfDay, OOTRegion | ||
|  | from BaseClasses import CollectionState as State | ||
|  | from .Utils import data_path, read_json | ||
|  | 
 | ||
|  | from worlds.generic.Rules import set_rule | ||
|  | 
 | ||
|  | 
 | ||
|  | escaped_items = {} | ||
|  | for item in item_table: | ||
|  |     escaped_items[re.sub(r'[\'()[\]]', '', item.replace(' ', '_'))] = item | ||
|  | 
 | ||
|  | event_name = re.compile(r'\w+') | ||
|  | # All generated lambdas must accept these keyword args! | ||
|  | # For evaluation at a certain age (required as all rules are evaluated at a specific age) | ||
|  | # or at a certain spot (can be omitted in many cases) | ||
|  | # or at a specific time of day (often unused) | ||
|  | kwarg_defaults = { | ||
|  |     # 'age': None, | ||
|  |     # 'spot': None, | ||
|  |     # 'tod': TimeOfDay.NONE, | ||
|  | } | ||
|  | 
 | ||
|  | allowed_globals = {'TimeOfDay': TimeOfDay} | ||
|  | 
 | ||
|  | rule_aliases = {} | ||
|  | nonaliases = set() | ||
|  | 
 | ||
|  | def load_aliases(): | ||
|  |     j = read_json(data_path('LogicHelpers.json')) | ||
|  |     for s, repl in j.items(): | ||
|  |         if '(' in s: | ||
|  |             rule, args = s[:-1].split('(', 1) | ||
|  |             args = [re.compile(r'\b%s\b' % a.strip()) for a in args.split(',')] | ||
|  |         else: | ||
|  |             rule = s | ||
|  |             args = () | ||
|  |         rule_aliases[rule] = (args, repl) | ||
|  |     nonaliases = escaped_items.keys() - rule_aliases.keys() | ||
|  | 
 | ||
|  | 
 | ||
|  | def isliteral(expr): | ||
|  |     return isinstance(expr, (ast.Num, ast.Str, ast.Bytes, ast.NameConstant)) | ||
|  | 
 | ||
|  | 
 | ||
|  | class Rule_AST_Transformer(ast.NodeTransformer): | ||
|  | 
 | ||
|  |     def __init__(self, world, player): | ||
|  |         self.world = world | ||
|  |         self.player = player | ||
|  |         self.events = set() | ||
|  |         # map Region -> rule ast string -> item name | ||
|  |         self.replaced_rules = defaultdict(dict) | ||
|  |         # delayed rules need to keep: region name, ast node, event name | ||
|  |         self.delayed_rules = [] | ||
|  |         # lazy load aliases | ||
|  |         if not rule_aliases: | ||
|  |             load_aliases() | ||
|  |         # final rule cache | ||
|  |         self.rule_cache = {} | ||
|  |         self.kwarg_defaults = kwarg_defaults.copy()  # otherwise this gets contaminated between players | ||
|  |         self.kwarg_defaults['player'] = self.player | ||
|  | 
 | ||
|  | 
 | ||
|  |     def visit_Name(self, node): | ||
|  |         if node.id in dir(self): | ||
|  |             return getattr(self, node.id)(node) | ||
|  |         elif node.id in rule_aliases: | ||
|  |             args, repl = rule_aliases[node.id] | ||
|  |             if args: | ||
|  |                 raise Exception('Parse Error: expected %d args for %s, not 0' % (len(args), node.id), | ||
|  |                         self.current_spot.name, ast.dump(node, False)) | ||
|  |             return self.visit(ast.parse(repl, mode='eval').body) | ||
|  |         elif node.id in escaped_items: | ||
|  |             return ast.Call( | ||
|  |                 func=ast.Attribute( | ||
|  |                     value=ast.Name(id='state', ctx=ast.Load()), | ||
|  |                     attr='has', | ||
|  |                     ctx=ast.Load()), | ||
|  |                 args=[ast.Str(escaped_items[node.id]), ast.Constant(self.player)], | ||
|  |                 keywords=[]) | ||
|  |         elif node.id in self.world.__dict__: | ||
|  |             # Settings are constant | ||
|  |             return ast.parse('%r' % self.world.__dict__[node.id], mode='eval').body | ||
|  |         elif node.id in State.__dict__: | ||
|  |             return self.make_call(node, node.id, [], []) | ||
|  |         elif node.id in self.kwarg_defaults or node.id in allowed_globals: | ||
|  |             return node | ||
|  |         elif event_name.match(node.id): | ||
|  |             self.events.add(node.id.replace('_', ' ')) | ||
|  |             return ast.Call( | ||
|  |                 func=ast.Attribute( | ||
|  |                     value=ast.Name(id='state', ctx=ast.Load()), | ||
|  |                     attr='has', | ||
|  |                     ctx=ast.Load()), | ||
|  |                 args=[ast.Str(node.id.replace('_', ' ')), ast.Constant(self.player)], | ||
|  |                 keywords=[]) | ||
|  |         else: | ||
|  |             raise Exception('Parse Error: invalid node name %s' % node.id, self.current_spot.name, ast.dump(node, False)) | ||
|  | 
 | ||
|  |     def visit_Str(self, node): | ||
|  |         return ast.Call( | ||
|  |             func=ast.Attribute( | ||
|  |                 value=ast.Name(id='state', ctx=ast.Load()), | ||
|  |                 attr='has', | ||
|  |                 ctx=ast.Load()), | ||
|  |             args=[ast.Str(node.s), ast.Constant(self.player)], | ||
|  |             keywords=[]) | ||
|  | 
 | ||
|  |     # python 3.8 compatibility: ast walking now uses visit_Constant for Constant subclasses | ||
|  |     # this includes Num, Str, NameConstant, Bytes, and Ellipsis. We only handle Str. | ||
|  |     def visit_Constant(self, node): | ||
|  |         if isinstance(node, ast.Str): | ||
|  |             return self.visit_Str(node) | ||
|  |         return node | ||
|  | 
 | ||
|  | 
 | ||
|  |     def visit_Tuple(self, node): | ||
|  |         if len(node.elts) != 2: | ||
|  |             raise Exception('Parse Error: Tuple must have 2 values', self.current_spot.name, ast.dump(node, False)) | ||
|  | 
 | ||
|  |         item, count = node.elts | ||
|  | 
 | ||
|  |         if not isinstance(item, (ast.Name, ast.Str)): | ||
|  |             raise Exception('Parse Error: first value must be an item. Got %s' % item.__class__.__name__, self.current_spot.name, ast.dump(node, False)) | ||
|  |         iname = item.id if isinstance(item, ast.Name) else item.s | ||
|  | 
 | ||
|  |         if not (isinstance(count, ast.Name) or isinstance(count, ast.Num)): | ||
|  |             raise Exception('Parse Error: second value must be a number. Got %s' % item.__class__.__name__, self.current_spot.name, ast.dump(node, False)) | ||
|  | 
 | ||
|  |         if isinstance(count, ast.Name): | ||
|  |             # Must be a settings constant | ||
|  |             count = ast.parse('%r' % self.world.__dict__[count.id], mode='eval').body | ||
|  | 
 | ||
|  |         if iname in escaped_items: | ||
|  |             iname = escaped_items[iname] | ||
|  | 
 | ||
|  |         if iname not in item_table: | ||
|  |             self.events.add(iname) | ||
|  | 
 | ||
|  |         return ast.Call( | ||
|  |             func=ast.Attribute( | ||
|  |                 value=ast.Name(id='state', ctx=ast.Load()), | ||
|  |                 attr='has', | ||
|  |                 ctx=ast.Load()), | ||
|  |             args=[ast.Str(iname), ast.Constant(self.player), count], | ||
|  |             keywords=[]) | ||
|  | 
 | ||
|  | 
 | ||
|  |     def visit_Call(self, node): | ||
|  |         if not isinstance(node.func, ast.Name): | ||
|  |             return node | ||
|  | 
 | ||
|  |         if node.func.id in dir(self): | ||
|  |             return getattr(self, node.func.id)(node) | ||
|  |         elif node.func.id in rule_aliases: | ||
|  |             args, repl = rule_aliases[node.func.id] | ||
|  |             if len(args) != len(node.args): | ||
|  |                 raise Exception('Parse Error: expected %d args for %s, not %d' % (len(args), node.func.id, len(node.args)), | ||
|  |                         self.current_spot.name, ast.dump(node, False)) | ||
|  |             # straightforward string manip | ||
|  |             for arg_re, arg_val in zip(args, node.args): | ||
|  |                 if isinstance(arg_val, ast.Name): | ||
|  |                     val = arg_val.id | ||
|  |                 elif isinstance(arg_val, ast.Constant): | ||
|  |                     val = repr(arg_val.value) | ||
|  |                 elif isinstance(arg_val, ast.Str): | ||
|  |                     val = repr(arg_val.s) | ||
|  |                 else: | ||
|  |                     raise Exception('Parse Error: invalid argument %s' % ast.dump(arg_val, False), | ||
|  |                             self.current_spot.name, ast.dump(node, False)) | ||
|  |                 repl = arg_re.sub(val, repl) | ||
|  |             return self.visit(ast.parse(repl, mode='eval').body) | ||
|  | 
 | ||
|  |         new_args = [] | ||
|  |         for child in node.args: | ||
|  |             if isinstance(child, ast.Name): | ||
|  |                 if child.id in self.world.__dict__: | ||
|  |                     # child = ast.Attribute( | ||
|  |                     #     value=ast.Attribute( | ||
|  |                     #         value=ast.Name(id='state', ctx=ast.Load()), | ||
|  |                     #         attr='world', | ||
|  |                     #         ctx=ast.Load()), | ||
|  |                     #     attr=child.id, | ||
|  |                     #     ctx=ast.Load()) | ||
|  |                     child = ast.Constant(getattr(self.world, child.id)) | ||
|  |                 elif child.id in rule_aliases: | ||
|  |                     child = self.visit(child) | ||
|  |                 elif child.id in escaped_items: | ||
|  |                     child = ast.Str(escaped_items[child.id]) | ||
|  |                 else: | ||
|  |                     child = ast.Str(child.id.replace('_', ' ')) | ||
|  |             elif not isinstance(child, ast.Str): | ||
|  |                 child = self.visit(child) | ||
|  |             new_args.append(child) | ||
|  | 
 | ||
|  |         return self.make_call(node, node.func.id, new_args, node.keywords) | ||
|  | 
 | ||
|  | 
 | ||
|  |     def visit_Subscript(self, node): | ||
|  |         if isinstance(node.value, ast.Name): | ||
|  |             s = node.slice if isinstance(node.slice, ast.Name) else node.slice.value | ||
|  |             return ast.Subscript( | ||
|  |                 value=ast.Attribute( | ||
|  |                     # value=ast.Attribute( | ||
|  |                     #     value=ast.Name(id='state', ctx=ast.Load()), | ||
|  |                     #     attr='world', | ||
|  |                     #     ctx=ast.Load()), | ||
|  |                     value=ast.Subscript( | ||
|  |                         value=ast.Attribute( | ||
|  |                             value=ast.Attribute( | ||
|  |                                 value=ast.Name(id='state', ctx=ast.Load()), | ||
|  |                                 attr='world', | ||
|  |                                 ctx=ast.Load()), | ||
|  |                             attr='worlds', | ||
|  |                             ctx=ast.Load()), | ||
|  |                         slice=ast.Index(value=ast.Constant(self.player)), | ||
|  |                         ctx=ast.Load()), | ||
|  |                     attr=node.value.id, | ||
|  |                     ctx=ast.Load()), | ||
|  |                 slice=ast.Index(value=ast.Str(s.id.replace('_', ' '))), | ||
|  |                 ctx=node.ctx) | ||
|  |         else: | ||
|  |             return node | ||
|  | 
 | ||
|  | 
 | ||
|  |     def visit_Compare(self, node): | ||
|  |         def escape_or_string(n): | ||
|  |             if isinstance(n, ast.Name) and n.id in escaped_items: | ||
|  |                 return ast.Str(escaped_items[n.id]) | ||
|  |             elif not isinstance(n, ast.Str): | ||
|  |                 return self.visit(n) | ||
|  |             return n | ||
|  | 
 | ||
|  |         # Fast check for json can_use | ||
|  |         if (len(node.ops) == 1 and isinstance(node.ops[0], ast.Eq) | ||
|  |                 and isinstance(node.left, ast.Name) and isinstance(node.comparators[0], ast.Name) | ||
|  |                 and node.left.id not in self.world.__dict__ and node.comparators[0].id not in self.world.__dict__): | ||
|  |             return ast.NameConstant(node.left.id == node.comparators[0].id) | ||
|  | 
 | ||
|  |         node.left = escape_or_string(node.left) | ||
|  |         node.comparators = list(map(escape_or_string, node.comparators)) | ||
|  |         node.ops = list(map(self.visit, node.ops)) | ||
|  | 
 | ||
|  |         # if all the children are literals now, we can evaluate | ||
|  |         if isliteral(node.left) and all(map(isliteral, node.comparators)): | ||
|  |             # either we turn the ops into operator functions to apply (lots of work), | ||
|  |             # or we compile, eval, and reparse the result | ||
|  |             try: | ||
|  |                 res = eval(compile(ast.fix_missing_locations(ast.Expression(node)), '<string>', 'eval')) | ||
|  |             except TypeError as e: | ||
|  |                 raise Exception('Parse Error: %s' % e, self.current_spot.name, ast.dump(node, False)) | ||
|  |             return self.visit(ast.parse('%r' % res, mode='eval').body) | ||
|  |         return node | ||
|  | 
 | ||
|  | 
 | ||
|  |     def visit_UnaryOp(self, node): | ||
|  |         # visit the children first | ||
|  |         self.generic_visit(node) | ||
|  |         # if all the children are literals now, we can evaluate | ||
|  |         if isliteral(node.operand): | ||
|  |             res = eval(compile(ast.Expression(node), '<string>', 'eval')) | ||
|  |             return ast.parse('%r' % res, mode='eval').body | ||
|  |         return node | ||
|  | 
 | ||
|  | 
 | ||
|  |     def visit_BinOp(self, node): | ||
|  |         # visit the children first | ||
|  |         self.generic_visit(node) | ||
|  |         # if all the children are literals now, we can evaluate | ||
|  |         if isliteral(node.left) and isliteral(node.right): | ||
|  |             res = eval(compile(ast.Expression(node), '<string>', 'eval')) | ||
|  |             return ast.parse('%r' % res, mode='eval').body | ||
|  |         return node | ||
|  | 
 | ||
|  | 
 | ||
|  |     def visit_BoolOp(self, node): | ||
|  |         # Everything else must be visited, then can be removed/reduced to. | ||
|  |         early_return = isinstance(node.op, ast.Or) | ||
|  |         groupable = 'has_any' if early_return else 'has_all' | ||
|  |         items = set() | ||
|  |         new_values = [] | ||
|  |         # if any elt is True(And)/False(Or), we can omit it | ||
|  |         # if any is False(And)/True(Or), the whole node can be replaced with it | ||
|  |         for elt in list(node.values): | ||
|  |             if isinstance(elt, ast.Str): | ||
|  |                 items.add(elt.s) | ||
|  |             elif isinstance(elt, ast.Name) and elt.id in nonaliases: | ||
|  |                 items.add(escaped_items[elt.id]) | ||
|  |             else: | ||
|  |                 # It's possible this returns a single item check, | ||
|  |                 # but it's already wrapped in a Call. | ||
|  |                 elt = self.visit(elt) | ||
|  |                 if isinstance(elt, ast.NameConstant): | ||
|  |                     if elt.value == early_return: | ||
|  |                         return elt | ||
|  |                     # else omit it | ||
|  |                 elif (isinstance(elt, ast.Call) and isinstance(elt.func, ast.Attribute) | ||
|  |                         and elt.func.attr in ('has', groupable) and len(elt.args) == 1): | ||
|  |                     args = elt.args[0] | ||
|  |                     if isinstance(args, ast.Str): | ||
|  |                         items.add(args.s) | ||
|  |                     else: | ||
|  |                         items.update(it.s for it in args.elts) | ||
|  |                 elif isinstance(elt, ast.BoolOp) and node.op.__class__ == elt.op.__class__: | ||
|  |                     new_values.extend(elt.values) | ||
|  |                 else: | ||
|  |                     new_values.append(elt) | ||
|  | 
 | ||
|  |         # package up the remaining items and values | ||
|  |         if not items and not new_values: | ||
|  |             # all values were True(And)/False(Or) | ||
|  |             return ast.NameConstant(not early_return) | ||
|  | 
 | ||
|  |         if items: | ||
|  |             node.values = [ast.Call( | ||
|  |                 func=ast.Attribute( | ||
|  |                     value=ast.Name(id='state', ctx=ast.Load()), | ||
|  |                     attr='has_any' if early_return else 'has_all', | ||
|  |                     ctx=ast.Load()), | ||
|  |                 args=[ast.Tuple(elts=[ast.Str(i) for i in items], ctx=ast.Load()), ast.Constant(self.player)], | ||
|  |                 keywords=[])] + new_values | ||
|  |         else: | ||
|  |             node.values = new_values | ||
|  |         if len(node.values) == 1: | ||
|  |             return node.values[0] | ||
|  |         return node | ||
|  | 
 | ||
|  | 
 | ||
|  |     # Generates an ast.Call invoking the given State function 'name', | ||
|  |     # providing given args and keywords, and adding in additional | ||
|  |     # keyword args from kwarg_defaults (age, etc.) | ||
|  |     def make_call(self, node, name, args, keywords): | ||
|  |         if not hasattr(State, name): | ||
|  |             raise Exception('Parse Error: No such function State.%s' % name, self.current_spot.name, ast.dump(node, False)) | ||
|  | 
 | ||
|  |         for (k, v) in self.kwarg_defaults.items(): | ||
|  |             keywords.append(ast.keyword(arg=f'{k}', value=ast.Constant(v))) | ||
|  | 
 | ||
|  |         return ast.Call( | ||
|  |             func=ast.Attribute( | ||
|  |                 value=ast.Name(id='state', ctx=ast.Load()), | ||
|  |                 attr=name, | ||
|  |                 ctx=ast.Load()), | ||
|  |             args=args, | ||
|  |             keywords=keywords) | ||
|  | 
 | ||
|  | 
 | ||
|  |     def replace_subrule(self, target, node): | ||
|  |         rule = ast.dump(node, False) | ||
|  |         if rule in self.replaced_rules[target]: | ||
|  |             return self.replaced_rules[target][rule] | ||
|  | 
 | ||
|  |         subrule_name = target + ' Subrule %d' % (1 + len(self.replaced_rules[target])) | ||
|  |         # Save the info to be made into a rule later | ||
|  |         self.delayed_rules.append((target, node, subrule_name)) | ||
|  |         # Replace the call with a reference to that item | ||
|  |         item_rule = ast.Call( | ||
|  |             func=ast.Attribute( | ||
|  |                 value=ast.Name(id='state', ctx=ast.Load()), | ||
|  |                 attr='has', | ||
|  |                 ctx=ast.Load()), | ||
|  |             args=[ast.Str(subrule_name), ast.Constant(self.player)], | ||
|  |             keywords=[]) | ||
|  |         # Cache the subrule for any others in this region | ||
|  |         # (and reserve the item name in the process) | ||
|  |         self.replaced_rules[target][rule] = item_rule | ||
|  |         return item_rule | ||
|  | 
 | ||
|  | 
 | ||
|  |     # Requires the target regions have been defined in the world. | ||
|  |     def create_delayed_rules(self): | ||
|  |         for region_name, node, subrule_name in self.delayed_rules: | ||
|  |             region = self.world.world.get_region(region_name, self.player) | ||
|  |             event = OOTLocation(self.player, subrule_name, type='Event', parent=region, internal=True) | ||
|  |             event.show_in_spoiler = False | ||
|  | 
 | ||
|  |             self.current_spot = event | ||
|  |             # This could, in theory, create further subrules. | ||
|  |             access_rule = self.make_access_rule(self.visit(node)) | ||
|  |             if access_rule is self.rule_cache.get('NameConstant(False)'): | ||
|  |                 event.access_rule = None | ||
|  |                 event.never = True | ||
|  |                 logging.getLogger('').debug('Dropping unreachable delayed event: %s', event.name) | ||
|  |             else: | ||
|  |                 if access_rule is self.rule_cache.get('NameConstant(True)'): | ||
|  |                     event.always = True | ||
|  |                 set_rule(event, access_rule) | ||
|  |                 region.locations.append(event) | ||
|  | 
 | ||
|  |                 self.world.make_event_item(subrule_name, event) | ||
|  |         # Safeguard in case this is called multiple times per world | ||
|  |         self.delayed_rules.clear() | ||
|  | 
 | ||
|  | 
 | ||
|  |     def make_access_rule(self, body): | ||
|  |         rule_str = ast.dump(body, False) | ||
|  |         if rule_str not in self.rule_cache: | ||
|  |             # requires consistent iteration on dicts | ||
|  |             kwargs = [ast.arg(arg=k) for k in self.kwarg_defaults.keys()] | ||
|  |             kwd = list(map(ast.Constant, self.kwarg_defaults.values())) | ||
|  |             try: | ||
|  |                 self.rule_cache[rule_str] = eval(compile( | ||
|  |                     ast.fix_missing_locations( | ||
|  |                         ast.Expression(ast.Lambda( | ||
|  |                             args=ast.arguments( | ||
|  |                                 posonlyargs=[], | ||
|  |                                 args=[ast.arg(arg='state')], | ||
|  |                                 defaults=[], | ||
|  |                                 kwonlyargs=kwargs, | ||
|  |                                 kw_defaults=kwd), | ||
|  |                             body=body))), | ||
|  |                     '<string>', 'eval'), | ||
|  |                     # globals/locals. if undefined, everything in the namespace *now* would be allowed | ||
|  |                     allowed_globals) | ||
|  |             except TypeError as e: | ||
|  |                 raise Exception('Parse Error: %s' % e, self.current_spot.name, ast.dump(body, False)) | ||
|  |         return self.rule_cache[rule_str] | ||
|  | 
 | ||
|  | 
 | ||
|  |     ## Handlers for specific internal functions used in the json logic. | ||
|  | 
 | ||
|  |     # at(region_name, rule) | ||
|  |     # Creates an internal event at the remote region and depends on it. | ||
|  |     def at(self, node): | ||
|  |         # Cache this under the target (region) name | ||
|  |         if len(node.args) < 2 or not isinstance(node.args[0], ast.Str): | ||
|  |             raise Exception('Parse Error: invalid at() arguments', self.current_spot.name, ast.dump(node, False)) | ||
|  |         return self.replace_subrule(node.args[0].s, node.args[1]) | ||
|  | 
 | ||
|  | 
 | ||
|  |     # here(rule) | ||
|  |     # Creates an internal event in the same region and depends on it. | ||
|  |     def here(self, node): | ||
|  |         if not node.args: | ||
|  |             raise Exception('Parse Error: missing here() argument', self.current_spot.name, ast.dump(node, False)) | ||
|  |         return self.replace_subrule( | ||
|  |                 self.current_spot.parent_region.name, | ||
|  |                 node.args[0]) | ||
|  | 
 | ||
|  |     ## Handlers for compile-time optimizations (former State functions) | ||
|  | 
 | ||
|  |     def at_day(self, node): | ||
|  |         if self.world.ensure_tod_access: | ||
|  |             # tod has DAY or (tod == NONE and (ss or find a path from a provider)) | ||
|  |             # parsing is better than constructing this expression by hand | ||
|  |             return ast.parse("(tod & TimeOfDay.DAY) if tod else (state.has_all(('Ocarina', 'Suns Song')) or state.search.can_reach(spot.parent_region, age=age, tod=TimeOfDay.DAY))", mode='eval').body | ||
|  |         return ast.NameConstant(True) | ||
|  | 
 | ||
|  |     def at_dampe_time(self, node): | ||
|  |         if self.world.ensure_tod_access: | ||
|  |             # tod has DAMPE or (tod == NONE and (find a path from a provider)) | ||
|  |             # parsing is better than constructing this expression by hand | ||
|  |             return ast.parse("(tod & TimeOfDay.DAMPE) if tod else state.search.can_reach(spot.parent_region, age=age, tod=TimeOfDay.DAMPE)", mode='eval').body | ||
|  |         return ast.NameConstant(True) | ||
|  | 
 | ||
|  |     def at_night(self, node): | ||
|  |         if self.current_spot.type == 'GS Token' and self.world.logic_no_night_tokens_without_suns_song: | ||
|  |             # Using visit here to resolve 'can_play' rule | ||
|  |             return self.visit(ast.parse('can_play(Suns_Song)', mode='eval').body) | ||
|  |         if self.world.ensure_tod_access: | ||
|  |             # tod has DAMPE or (tod == NONE and (ss or find a path from a provider)) | ||
|  |             # parsing is better than constructing this expression by hand | ||
|  |             return ast.parse("(tod & TimeOfDay.DAMPE) if tod else (state.has_all(('Ocarina', 'Suns Song')) or state.search.can_reach(spot.parent_region, age=age, tod=TimeOfDay.DAMPE))", mode='eval').body | ||
|  |         return ast.NameConstant(True) | ||
|  | 
 | ||
|  | 
 | ||
|  |     # Parse entry point | ||
|  |     # If spot is None, here() rules won't work. | ||
|  |     def parse_rule(self, rule_string, spot=None): | ||
|  |         self.current_spot = spot | ||
|  |         return self.make_access_rule(self.visit(ast.parse(rule_string, mode='eval').body)) | ||
|  | 
 | ||
|  |     def parse_spot_rule(self, spot): | ||
|  |         rule = spot.rule_string.split('#', 1)[0].strip() | ||
|  | 
 | ||
|  |         access_rule = self.parse_rule(rule, spot) | ||
|  |         set_rule(spot, access_rule) | ||
|  |         if access_rule is self.rule_cache.get('NameConstant(False)'): | ||
|  |             spot.never = True | ||
|  |         elif access_rule is self.rule_cache.get('NameConstant(True)'): | ||
|  |             spot.always = True | ||
|  | 
 | ||
|  |     # Hijacking functions | ||
|  |     def current_spot_child_access(self, node):  | ||
|  |         r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region | ||
|  |         return ast.parse(f"state._oot_reach_as_age('{r.name}', 'child', {self.player})", mode='eval').body | ||
|  | 
 | ||
|  |     def current_spot_adult_access(self, node):  | ||
|  |         r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region | ||
|  |         return ast.parse(f"state._oot_reach_as_age('{r.name}', 'adult', {self.player})", mode='eval').body | ||
|  | 
 | ||
|  |     def current_spot_starting_age_access(self, node):  | ||
|  |         return self.current_spot_child_access(node) if self.world.starting_age == 'child' else self.current_spot_adult_access(node) | ||
|  | 
 | ||
|  |     def has_bottle(self, node):  | ||
|  |         return ast.parse(f"state._oot_has_bottle({self.player})", mode='eval').body | ||
|  | 
 | ||
|  |     def can_live_dmg(self, node): | ||
|  |         return ast.parse(f"state._oot_can_live_dmg({self.player}, {node.args[0].value})", mode='eval').body |