 51c38fc628
			
		
	
	51c38fc628
	
	
	
		
			
			* first commit (not including OoT data files yet) * added some basic options * rule parser works now at least * make sure to commit everything this time * temporary change to BaseClasses for oot * overworld location graph builds mostly correctly * adding oot data files * commenting out world options until later since they only existed to make the RuleParser work * conversion functions between AP ids and OOT ids * world graph outputs * set scrub prices * itempool generates, entrances connected, way too many options added * fixed set_rules and set_shop_rules * temp baseclasses changes * Reaches the fill step now, old event-based system retained in case the new way breaks * Song placements and misc fixes everywhere * temporary changes to make oot work * changed root exits for AP fill framework * prevent infinite recursion due to OoT sharing usage of the address field * age reachability works hopefully, songs are broken again * working spoiler log generation on beatable-only * Logic tricks implemented * need this for logic tricks * fixed map/compass being placed on Serenade location * kill unreachable events before filling the world * add a bunch of utility functions to prepare for rom patching * move OptionList into generic options * fixed some silly bugs with OptionList * properly seed all random behavior (so far) * ROM generation working * fix hints trying to get alttp dungeon hint texts * continue fixing hints * add oot to network data package * change item and location IDs to 66000 and 67000 range respectively * push removed items to precollected items * fixed various issues with cross-contamination with multiple world generation * reenable glitched logic (hopefully) * glitched world files age-check fix * cleaned up some get_locations calls * added token shuffle and scrub shuffle, modified some options slightly to make the parsing work * reenable MQ dungeons * fix forest mq exception * made targeting style an option for now, will be cosmetic later * reminder to move targeting to cosmetics * some oot option maintenance * enabled starting time of day * fixed issue breaking shop slots in multiworld generation * added "off" option for text shuffle and hints * shopsanity functionality restored * change patch file extension * remove unnecessary utility functions + imports * update MIT license * change option to "patch_uncompressed_rom" instead of "compress_rom" * compliance with new AutoWorld systems * Kill only internal events, remove non-internal big poe event in code * re-add the big poe event and handle it correctly * remove extra method in Range option * fix typo * Starting items, starting with consumables option * do not remove nonexistent item * move set_shop_rules to after shop items are placed * some cleanup * add retries for song placement * flagged Skull Mask and Mask of Truth as advancement items * update OoT to use LogicMixin * Fixed trying to assign starting items from the wrong players * fixed song retry step * improved option handling, comments, and starting item replacements * DefaultOnToggle writes Yes or No to spoiler * enable compression of output if Compress executable is present * clean up compression * check whether (de)compressor exists before running the process * allow specification of rom path in host.yaml * check if decompressed file already exists before decompressing again * fix triforce hunt generation * rename all the oot state functions with prefix * OoT: mark triforce pieces as completion goal for triforce hunt * added overworld and any-dungeon shuffle for dungeon items * Hide most unshuffled locations and events from the list of locations in spoiler * build oot option ranges with a generic function instead of defining each separately * move oot output-type control to host.yaml instead of individual yamls * implement dungeon song shuffle * minor improvements to overworld dungeon item shuffle * remove random ice trap names in shops, mostly to avoid maintaining a massive censor list * always output patch file to folder, remove option to generate ROM in preparation for removal * re-add the fix for infinite recursion due to not being light or dark world * change AP-sendable to Ocarina of Time model, since the triforce piece has some extra code apparently * oot: remove item_names and location_names * oot: minor fixes * oot: comment out ROM patching * oot: only add CollectionState objects on creation if actually needed * main entrance shuffle method and entrances-based rules * fix entrances based rules * disable master quest and big poe count options for client compatibility * use get_player_name instead of get_player_names * fix OptionList * fix oot options for new option system * new coop section in oot rom: expand player names to 16 bytes, write AP_PLAYER_NAME at end of PLAYER_NAMES * fill AP player name in oot rom with 0 instead of 0xDF * encode player name with ASCII for fixed-width * revert oot player name array to 8 bytes per name * remove Pierre location if fast scarecrow is on * check player name length * "free_scarecrow" not "fast_scarecrow" * OoT locations now properly store the AP ID instead of the oot internal ID * oot __version__ updates in lockstep with AP version * pull in unmodified oot cosmetic files * also grab JSONDump since it's needed apparently * gather extra needed methods, modify imports * delete cosmetics log, replace all instances of SettingsList with OOTWorld * cosmetic options working, except for sound effects (due to ear-safe issues) * SFX, Music, and Fanfare randomization reenabled * move OoT data files into the worlds folder * move Compress and Decompress into oot data folder * Replace get_all_state with custom method to avoid the cache * OoT ROM: increment item counter before setting incoming item/player values to 0, preventing desync issues * set data_version to 0 * make Kokiri Sword shuffle off by default * reenable "Random Choice" for various cosmetic options * kill Ruto's Letter turnin if open fountain also fix for shopsanity * place Buy Goron/Zora Tunic first in shop shuffle * make ice traps appear as other items instead of breaking generation * managed to break ice traps on non-major-only * only handle ice traps if they are on * fix shopsanity for non-oot games, and write player name instead of player number * light arrows hint uses player name instead of player number * Reenable "skip child zelda" option * fix entrances_based_rules * fix ganondorf hint if starting with light arrows * fix dungeonitem shuffle and shopsanity interaction * remove has_all_of, has_any_of, count_of in BaseClasses, replace usage with has_all, has_any, has_group * force local giveable item on ZL if skip_child_zelda and shuffle_song_items is any * keep bosses and bombchu bowling chus out of data package * revert workaround for infinite recursion and fix it properly * fix shared shop id caches during patching process * fix shop text box overflows, as much as possible * add default oot host.yaml option * add .apz5, .n64, .z64 to gitignore * Properly document and name all (functioning) OOT options * clean up some imports * remove unnecessary files from oot's data * fix typo in gitignore * readd the Compress and Decompress utilities, since they are needed for generation * cleanup of imports and some minor optimizations * increase shop offset for item IDs to 0xCB * remove shop item AP ids entirely * prevent triforce pieces for other players from being received by yourself * add "excluded" property to Location * Hint system adapted and reenabled; hints still unseeded * make hints deterministic with lists instead of sets * do not allow hints to point to Light Arrows on non-vanilla bridge * foreign locations hint as their full name in OoT rather than their region * checkedLocations now stores hint names by player ID, so that the same location in different worlds can have hints associated * consolidate versioning in Utils * ice traps appear as major items rather than any progression item * set prescription and claim check as defaults for adult trade item settings * add oot options to playerSettings * allow case-insensitive logic tricks in yaml * fix oot shopsanity option formatting * Write OoT override info even if local item, enabling local checks to show up immediately in the client * implement CollectionState.can_live_dmg for oot glitched logic * filter item names for invalid characters when patching shops * make ice traps appear according to the settings of the world they are shuffled into, rather than the original world * set hidden-spoiler items and locations with Shop items to events * make GF carpenters, Gerudo Card, Malon, ZL, and Impa events if the relevant settings are enabled, preventing them from appearing in the client on game start * Fix oot Glitched and No Logic generation * fix indenting * Greatly reduce displayed cosmetic options * Change oot data version to 1 * add apz5 distribution to webhost * print player name if an ALttP dungeon contains a good item for OoT world * delete unneeded commented code * remove OcarinaSongs import to satisfy lint
		
			
				
	
	
		
			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
 |