diff --git a/Main.py b/Main.py index c4db4b48..bbee93b4 100644 --- a/Main.py +++ b/Main.py @@ -23,6 +23,7 @@ from worlds.alttp.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_ from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple from worlds.hk import gen_hollow, gen_regions, set_rules as set_hk_rules +from worlds.generic.Rules import locality_rules import Patch seeddigits = 20 @@ -205,7 +206,9 @@ def main(args, seed=None): generate_itempool(world, player) logger.info('Calculating Access Rules.') - + if world.players > 1: + for player in world.player_ids: + locality_rules(world, player) for player in world.alttp_player_ids: set_rules(world, player) diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index ba58c754..85ef3d30 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -9,7 +9,7 @@ from worlds.alttp.Dungeons import get_dungeon_item_pool from worlds.alttp.EntranceShuffle import connect_entrance from Fill import FillError, fill_restrictive from worlds.alttp.Items import ItemFactory, GetBeemizerItem -from worlds.alttp.Rules import forbid_items_for_player +from worlds.generic.Rules import forbid_items_for_player # This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space. # Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided. diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index f59e42f2..935e5bb3 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -29,7 +29,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \ SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_bundled -from worlds.alttp.Items import ItemFactory +from worlds.alttp.Items import ItemFactory, item_table from worlds.alttp.EntranceShuffle import door_addresses import Patch @@ -1643,9 +1643,14 @@ def write_custom_shops(rom, world, player): slot = 0 if shop.type == ShopType.TakeAny else index if item is None: break - if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]: - rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask) - item_data = [shop_id, ItemFactory(item['item'], player).code] + int16_as_bytes(item['price']) + \ + if not item['item'] in item_table: # item not native to ALTTP + item_code = 0x21 + else: + item_code = ItemFactory(item['item'], player).code + if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]: + rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask) + + item_data = [shop_id, item_code] + int16_as_bytes(item['price']) + \ [item['max'], ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF] + \ int16_as_bytes(item['replacement_price']) + [0 if item['player'] == player else item['player']] items_data.extend(item_data) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index ec136f3d..0e853f20 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -5,11 +5,11 @@ from BaseClasses import RegionType, MultiWorld, Entrance from worlds.alttp.Items import ItemFactory, progression_items, item_name_groups from worlds.alttp.OverworldGlitchRules import overworld_glitches_rules, no_logic_rules from worlds.alttp.Bosses import GanonDefeatRule +from worlds.generic.Rules import set_rule, add_rule, forbid_item, add_item_rule, item_in_locations, \ + item_name def set_rules(world, player): - if world.players > 1: - locality_rules(world, player) if world.logic[player] == 'nologic': logging.info( 'WARNING! Seeds generated under this logic often require major glitches and may be impossible!') @@ -111,8 +111,6 @@ def mirrorless_path_to_castle_courtyard(world, player): raise Exception(f"Could not find mirrorless path to castle courtyard for Player {player} ({world.get_player_names(player)})") -def set_rule(spot, rule): - spot.access_rule = rule def set_defeat_dungeon_boss_rule(location): # Lambda required to defer evaluation of dungeon.boss since it will change later if boss shuffle is used @@ -121,13 +119,6 @@ def set_defeat_dungeon_boss_rule(location): def set_always_allow(spot, rule): spot.always_allow = rule -def add_rule(spot, rule, combine='and'): - old_rule = spot.access_rule - if combine == 'or': - spot.access_rule = lambda state: rule(state) or old_rule(state) - else: - spot.access_rule = lambda state: rule(state) and old_rule(state) - def add_lamp_requirement(world: MultiWorld, spot, player: int, has_accessible_torch: bool = False): if world.dark_room_logic[player] == "lamp": @@ -143,51 +134,6 @@ def add_lamp_requirement(world: MultiWorld, spot, player: int, has_accessible_to raise ValueError(f"Unknown Dark Room Logic: {world.dark_room_logic[player]}") -def forbid_item(location, item, player: int): - old_rule = location.item_rule - location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i) - - -def forbid_items_for_player(location, items: set, player: int): - old_rule = location.item_rule - location.item_rule = lambda i: (i.player != player or i.name not in items) and old_rule(i) - -def forbid_items(location, items: set): - """unused, but kept as a debugging tool.""" - old_rule = location.item_rule - location.item_rule = lambda i: i.name not in items and old_rule(i) - - -def add_item_rule(location, rule): - old_rule = location.item_rule - location.item_rule = lambda item: rule(item) and old_rule(item) - - -def item_in_locations(state, item, player, locations): - for location in locations: - if item_name(state, location[0], location[1]) == (item, player): - return True - return False - - -def item_name(state, location, player): - location = state.world.get_location(location, player) - if location.item is None: - return None - return (location.item.name, location.item.player) - - -def locality_rules(world, player): - if world.local_items[player]: - for location in world.get_locations(): - if location.player != player: - forbid_items_for_player(location, world.local_items[player], player) - if world.non_local_items[player]: - for location in world.get_locations(): - if location.player == player: - forbid_items_for_player(location, world.non_local_items[player], player) - - non_crossover_items = (item_name_groups["Small Keys"] | item_name_groups["Big Keys"] | progression_items) - { "Small Key (Universal)"} @@ -279,7 +225,7 @@ def global_rules(world, player): ep_prize.parent_region.dungeon.boss.can_defeat(state)) if not world.enemy_shuffle[player]: add_rule(ep_boss, lambda state: state.can_shoot_arrows(player)) - add_rule(ep_prize,lambda state: state.can_shoot_arrows(player)) + add_rule(ep_prize, lambda state: state.can_shoot_arrows(player)) set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player)) set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has_Boots(player)) @@ -328,7 +274,8 @@ def global_rules(world, player): set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player)) set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state.has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state.has_key('Small Key (Ice Palace)', player, 1)))) # TODO: investigate change from VT. Changed to hookshot or 2 keys (no checking for big key in specific chests) - set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or (item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state.has_key('Small Key (Ice Palace)', player))) and (state.world.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player))) + set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or ( + item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state.has_key('Small Key (Ice Palace)', player))) and (state.world.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player))) set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player)) set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has_Boots(player) or state.has('Hookshot', player)) and (state.has_sword(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player))) # need to defeat wizzrobes, bombs don't work ... @@ -340,8 +287,10 @@ def global_rules(world, player): set_rule(world.get_location('Misery Mire - Map Chest', player), lambda state: state.has_key('Small Key (Misery Mire)', player, 1) or state.has('Big Key (Misery Mire)', player)) set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state.has_key('Small Key (Misery Mire)', player, 1) or state.has_key('Big Key (Misery Mire)', player)) # we can place a small key in the West wing iff it also contains/blocks the Big Key, as we cannot reach and softlock with the basement key door yet - set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state.has_key('Small Key (Misery Mire)', player, 2) if ((item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or - (item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state.has_key('Small Key (Misery Mire)', player, 3)) + set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state.has_key('Small Key (Misery Mire)', player, 2) if (( + item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or + ( + item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state.has_key('Small Key (Misery Mire)', player, 3)) set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: state.has_fire_source(player)) set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player)) @@ -370,11 +319,13 @@ def global_rules(world, player): set_rule(world.get_entrance('Palace of Darkness (North)', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 4)) set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player)) - set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) or (item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state.has_key('Small Key (Palace of Darkness)', player, 3))) + set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) or ( + item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state.has_key('Small Key (Palace of Darkness)', player, 3))) if world.accessibility[player] != 'locations': set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state.has_key('Small Key (Palace of Darkness)', player, 5)) - set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) or (item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state.has_key('Small Key (Palace of Darkness)', player, 4))) + set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) or ( + item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state.has_key('Small Key (Palace of Darkness)', player, 4))) if world.accessibility[player] != 'locations': set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state.has_key('Small Key (Palace of Darkness)', player, 5)) @@ -387,7 +338,8 @@ def global_rules(world, player): set_rule(world.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has_Boots(player)) set_rule(world.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has_Boots(player))) - set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 4) or (item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state.has_key('Small Key (Ganons Tower)', player, 3))) + set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 4) or ( + item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state.has_key('Small Key (Ganons Tower)', player, 3))) if world.accessibility[player] != 'locations': set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state.has_key('Small Key (Ganons Tower)', player, 3) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player)) @@ -398,15 +350,18 @@ def global_rules(world, player): set_rule(world.get_entrance('Ganons Tower (Firesnake Room)', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 3)) #The actual requirements for these rooms to avoid key-lock - set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 3) or ((item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_in_locations(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state.has_key('Small Key (Ganons Tower)', player, 2))) + set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 3) or (( + item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_in_locations(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state.has_key('Small Key (Ganons Tower)', player, 2))) for location in randomizer_room_chests: - set_rule(world.get_location(location, player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 4) or (item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state.has_key('Small Key (Ganons Tower)', player, 3))) + set_rule(world.get_location(location, player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 4) or ( + item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state.has_key('Small Key (Ganons Tower)', player, 3))) # Once again it is possible to need more than 3 keys... set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 3) and state.has('Fire Rod', player)) # Actual requirements for location in compass_room_chests: - set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state.has_key('Small Key (Ganons Tower)', player, 4) or (item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state.has_key('Small Key (Ganons Tower)', player, 3)))) + set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state.has_key('Small Key (Ganons Tower)', player, 4) or ( + item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state.has_key('Small Key (Ganons Tower)', player, 3)))) set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player)) diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py new file mode 100644 index 00000000..2316f008 --- /dev/null +++ b/worlds/generic/Rules.py @@ -0,0 +1,56 @@ +def locality_rules(world, player): + if world.local_items[player]: + for location in world.get_locations(): + if location.player != player: + forbid_items_for_player(location, world.local_items[player], player) + if world.non_local_items[player]: + for location in world.get_locations(): + if location.player == player: + forbid_items_for_player(location, world.non_local_items[player], player) + + +def set_rule(spot, rule): + spot.access_rule = rule + + +def add_rule(spot, rule, combine='and'): + old_rule = spot.access_rule + if combine == 'or': + spot.access_rule = lambda state: rule(state) or old_rule(state) + else: + spot.access_rule = lambda state: rule(state) and old_rule(state) + + +def forbid_item(location, item, player: int): + old_rule = location.item_rule + location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i) + + +def forbid_items_for_player(location, items: set, player: int): + old_rule = location.item_rule + location.item_rule = lambda i: (i.player != player or i.name not in items) and old_rule(i) + + +def forbid_items(location, items: set): + """unused, but kept as a debugging tool.""" + old_rule = location.item_rule + location.item_rule = lambda i: i.name not in items and old_rule(i) + + +def add_item_rule(location, rule): + old_rule = location.item_rule + location.item_rule = lambda item: rule(item) and old_rule(item) + + +def item_in_locations(state, item, player, locations): + for location in locations: + if item_name(state, location[0], location[1]) == (item, player): + return True + return False + + +def item_name(state, location, player): + location = state.world.get_location(location, player) + if location.item is None: + return None + return (location.item.name, location.item.player) \ No newline at end of file