LttP: move more stuff out of core (#5049)

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
Fabian Dill
2025-08-01 22:30:30 +02:00
committed by GitHub
parent 37a9d94865
commit 9ad6959559
7 changed files with 70 additions and 71 deletions

View File

@@ -154,17 +154,11 @@ class MultiWorld():
self.algorithm = 'balanced'
self.groups = {}
self.regions = self.RegionManager(players)
self.shops = []
self.itempool = []
self.seed = None
self.seed_name: str = "Unavailable"
self.precollected_items = {player: [] for player in self.player_ids}
self.required_locations = []
self.light_world_light_cone = False
self.dark_world_light_cone = False
self.rupoor_cost = 10
self.aga_randomness = True
self.save_and_quit_from_boss = True
self.custom = False
self.customitemarray = []
self.shuffle_ganon = True

View File

@@ -223,7 +223,7 @@ items_reduction_table = (
def generate_itempool(world):
player = world.player
player: int = world.player
multiworld = world.multiworld
if world.options.item_pool.current_key not in difficulties:
@@ -280,7 +280,6 @@ def generate_itempool(world):
if multiworld.custom:
pool, placed_items, precollected_items, clock_mode, treasure_hunt_required = (
make_custom_item_pool(multiworld, player))
multiworld.rupoor_cost = min(multiworld.customitemarray[67], 9999)
else:
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_required, treasure_hunt_total,
additional_triforce_pieces) = get_pool_core(multiworld, player)
@@ -386,8 +385,8 @@ def generate_itempool(world):
if world.options.retro_bow:
shop_items = 0
shop_locations = [location for shop_locations in (shop.region.locations for shop in multiworld.shops if
shop.type == ShopType.Shop and shop.region.player == player) for location in shop_locations if
shop_locations = [location for shop_locations in (shop.region.locations for shop in world.shops if
shop.type == ShopType.Shop) for location in shop_locations if
location.shop_slot is not None]
for location in shop_locations:
if location.shop.inventory[location.shop_slot]["item"] == "Single Arrow":
@@ -546,7 +545,7 @@ def set_up_take_anys(multiworld, world, player):
connect_entrance(multiworld, entrance.name, old_man_take_any.name, player)
entrance.target = 0x58
old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True, total_shop_slots)
multiworld.shops.append(old_man_take_any.shop)
world.shops.append(old_man_take_any.shop)
sword_indices = [
index for index, item in enumerate(multiworld.itempool) if item.player == player and item.type == 'Sword'
@@ -574,7 +573,7 @@ def set_up_take_anys(multiworld, world, player):
connect_entrance(multiworld, entrance.name, take_any.name, player)
entrance.target = target
take_any.shop = TakeAny(take_any, room_id, 0xE3, True, True, total_shop_slots + num + 1)
multiworld.shops.append(take_any.shop)
world.shops.append(take_any.shop)
take_any.shop.add_inventory(0, 'Blue Potion', 0, 0)
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0)
location = ALttPLocation(player, take_any.name, shop_table_by_location[take_any.name], parent=take_any)

View File

@@ -1002,14 +1002,19 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# set light cones
rom.write_byte(0x180038, 0x01 if local_world.options.mode == "standard" else 0x00)
rom.write_byte(0x180039, 0x01 if world.light_world_light_cone else 0x00)
rom.write_byte(0x18003A, 0x01 if world.dark_world_light_cone else 0x00)
# light world light cone
rom.write_byte(0x180039, local_world.light_world_light_cone)
# dark world light cone
rom.write_byte(0x18003A, local_world.dark_world_light_cone)
GREEN_TWENTY_RUPEES = 0x47
GREEN_CLOCK = item_table["Green Clock"].item_code
rom.write_byte(0x18004F, 0x01) # Byrna Invulnerability: on
# Rupoor negative value
rom.write_int16(0x180036, local_world.rupoor_cost)
# handle item_functionality
if local_world.options.item_functionality == 'hard':
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
@@ -1027,8 +1032,6 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# Disable catching fairies
rom.write_byte(0x34FD6, 0x80)
overflow_replacement = GREEN_TWENTY_RUPEES
# Rupoor negative value
rom.write_int16(0x180036, world.rupoor_cost)
# Set stun items
rom.write_byte(0x180180, 0x02) # Hookshot only
elif local_world.options.item_functionality == 'expert':
@@ -1047,8 +1050,6 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
# Disable catching fairies
rom.write_byte(0x34FD6, 0x80)
overflow_replacement = GREEN_TWENTY_RUPEES
# Rupoor negative value
rom.write_int16(0x180036, world.rupoor_cost)
# Set stun items
rom.write_byte(0x180180, 0x00) # Nothing
else:
@@ -1066,8 +1067,6 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0x18004F, 0x01)
# Enable catching fairies
rom.write_byte(0x34FD6, 0xF0)
# Rupoor negative value
rom.write_int16(0x180036, world.rupoor_cost)
# Set stun items
rom.write_byte(0x180180, 0x03) # All standard items
# Set overflow items for progressive equipment
@@ -1313,7 +1312,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0x18008C, 0x01 if local_world.options.crystals_needed_for_gt == 0 else 0x00) # GT pre-opened if crystal requirement is 0
rom.write_byte(0xF5D73, 0xF0) # bees are catchable
rom.write_byte(0xF5F10, 0xF0) # bees are catchable
rom.write_byte(0x180086, 0x00 if world.aga_randomness else 0x01) # set blue ball and ganon warp randomness
rom.write_byte(0x180086, 0x00) # set blue ball and ganon warp randomness
rom.write_byte(0x1800A0, 0x01) # return to light world on s+q without mirror
rom.write_byte(0x1800A1, 0x01) # enable overworld screen transition draining for water level inside swamp
rom.write_byte(0x180174, 0x01 if local_world.fix_fake_world else 0x00)
@@ -1618,7 +1617,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills
rom.write_byte(0x1800A4, 0x01 if local_world.options.glitches_required != 'no_logic' else 0x00) # enable POD EG fix
rom.write_byte(0x186383, 0x01 if local_world.options.glitches_required == 'no_logic' else 0x00) # disable glitching to Triforce from Ganons Room
rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill
rom.write_byte(0x180042, 0x01 if local_world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill
# remove shield from uncle
rom.write_bytes(0x6D253, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
@@ -1739,8 +1738,7 @@ def get_price_data(price: int, price_type: int) -> List[int]:
def write_custom_shops(rom, world, player):
shops = sorted([shop for shop in world.shops if shop.custom and shop.region.player == player],
key=lambda shop: shop.sram_offset)
shops = sorted([shop for shop in world.worlds[player].shops if shop.custom], key=lambda shop: shop.sram_offset)
shop_data = bytearray()
items_data = bytearray()

View File

@@ -147,7 +147,6 @@ def set_defeat_dungeon_boss_rule(location):
add_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state))
def set_always_allow(spot, rule):
spot.always_allow = rule
@@ -980,18 +979,19 @@ def check_is_dark_world(region):
return False
def add_conditional_lamps(world, player):
def add_conditional_lamps(multiworld, player):
# Light cones in standard depend on which world we actually are in, not which one the location would normally be
# We add Lamp requirements only to those locations which lie in the dark world (or everything if open
local_world = multiworld.worlds[player]
def add_conditional_lamp(spot, region, spottype='Location', accessible_torch=False):
if (not world.dark_world_light_cone and check_is_dark_world(world.get_region(region, player))) or (
not world.light_world_light_cone and not check_is_dark_world(world.get_region(region, player))):
if (not local_world.dark_world_light_cone and check_is_dark_world(local_world.get_region(region))) or (
not local_world.light_world_light_cone and not check_is_dark_world(local_world.get_region(region))):
if spottype == 'Location':
spot = world.get_location(spot, player)
spot = local_world.get_location(spot)
else:
spot = world.get_entrance(spot, player)
add_lamp_requirement(world, spot, player, accessible_torch)
spot = local_world.get_entrance(spot)
add_lamp_requirement(multiworld, spot, player, accessible_torch)
add_conditional_lamp('Misery Mire (Vitreous)', 'Misery Mire (Entrance)', 'Entrance')
add_conditional_lamp('Turtle Rock (Dark Room) (North)', 'Turtle Rock (Entrance)', 'Entrance')
@@ -1002,7 +1002,7 @@ def add_conditional_lamps(world, player):
'Location', True)
add_conditional_lamp('Palace of Darkness - Dark Basement - Right', 'Palace of Darkness (Entrance)',
'Location', True)
if world.worlds[player].options.mode != 'inverted':
if multiworld.worlds[player].options.mode != 'inverted':
add_conditional_lamp('Agahnim 1', 'Agahnims Tower', 'Entrance')
add_conditional_lamp('Castle Tower - Dark Maze', 'Agahnims Tower')
add_conditional_lamp('Castle Tower - Dark Archer Key Drop', 'Agahnims Tower')
@@ -1024,10 +1024,10 @@ def add_conditional_lamps(world, player):
add_conditional_lamp('Eastern Palace - Boss', 'Eastern Palace', 'Location', True)
add_conditional_lamp('Eastern Palace - Prize', 'Eastern Palace', 'Location', True)
if not world.worlds[player].options.mode == "standard":
add_lamp_requirement(world, world.get_location('Sewers - Dark Cross', player), player)
add_lamp_requirement(world, world.get_entrance('Sewers Back Door', player), player)
add_lamp_requirement(world, world.get_entrance('Throne Room', player), player)
if not multiworld.worlds[player].options.mode == "standard":
add_lamp_requirement(multiworld, local_world.get_location("Sewers - Dark Cross"), player)
add_lamp_requirement(multiworld, local_world.get_entrance("Sewers Back Door"), player)
add_lamp_requirement(multiworld, local_world.get_entrance("Throne Room"), player)
def open_rules(world, player):

View File

@@ -14,8 +14,6 @@ from .Items import item_name_groups
from .StateHelpers import has_hearts, can_use_bombs, can_hold_arrows
logger = logging.getLogger("Shops")
@unique
class ShopType(IntEnum):
@@ -162,7 +160,10 @@ shop_class_mapping = {ShopType.UpgradeShop: UpgradeShop,
def push_shop_inventories(multiworld):
shop_slots = [location for shop_locations in (shop.region.locations for shop in multiworld.shops if shop.type
all_shops = []
for world in multiworld.get_game_worlds(ALttPLocation.game):
all_shops.extend(world.shops)
shop_slots = [location for shop_locations in (shop.region.locations for shop in all_shops if shop.type
!= ShopType.TakeAny) for location in shop_locations if location.shop_slot is not None]
for location in shop_slots:
@@ -178,7 +179,7 @@ def push_shop_inventories(multiworld):
get_price(multiworld, location.shop.inventory[location.shop_slot], location.player,
location.shop_price_type)[1])
for world in multiworld.get_game_worlds("A Link to the Past"):
for world in multiworld.get_game_worlds(ALttPLocation.game):
world.pushed_shop_inventories.set()
@@ -225,7 +226,7 @@ def create_shops(multiworld, player: int):
if locked is None:
shop.locked = True
region.shop = shop
multiworld.shops.append(shop)
multiworld.worlds[player].shops.append(shop)
for index, item in enumerate(inventory):
shop.add_inventory(index, *item)
if not locked and (num_slots or type == ShopType.UpgradeShop):
@@ -309,50 +310,50 @@ def set_up_shops(multiworld, player: int):
from .Options import small_key_shuffle
# TODO: move hard+ mode changes for shields here, utilizing the new shops
if multiworld.worlds[player].options.retro_bow:
local_world = multiworld.worlds[player]
if local_world.options.retro_bow:
rss = multiworld.get_region('Red Shield Shop', player).shop
# Can't just replace the single arrow with 10 arrows as retro doesn't need them.
replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50],
['Blue Shield', 50], ['Small Heart',
10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them.
if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
['Blue Shield', 50], ['Small Heart', 10]]
if local_world.options.small_key_shuffle == small_key_shuffle.option_universal:
replacement_items.append(['Small Key (Universal)', 100])
replacement_item = multiworld.random.choice(replacement_items)
rss.add_inventory(2, 'Single Arrow', 80, 1, replacement_item[0], replacement_item[1])
rss.locked = True
if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal or multiworld.worlds[player].options.retro_bow:
for shop in multiworld.random.sample([s for s in multiworld.shops if
s.custom and not s.locked and s.type == ShopType.Shop
and s.region.player == player], 5):
if local_world.options.small_key_shuffle == small_key_shuffle.option_universal or local_world.options.retro_bow:
for shop in multiworld.random.sample([s for s in local_world.shops if
s.custom and not s.locked and s.type == ShopType.Shop], 5):
shop.locked = True
slots = [0, 1, 2]
multiworld.random.shuffle(slots)
slots = iter(slots)
if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
if local_world.options.small_key_shuffle == small_key_shuffle.option_universal:
shop.add_inventory(next(slots), 'Small Key (Universal)', 100)
if multiworld.worlds[player].options.retro_bow:
if local_world.options.retro_bow:
shop.push_inventory(next(slots), 'Single Arrow', 80)
if multiworld.worlds[player].options.shuffle_capacity_upgrades:
for shop in multiworld.shops:
if shop.type == ShopType.UpgradeShop and shop.region.player == player and \
if local_world.options.shuffle_capacity_upgrades:
for shop in local_world.shops:
if shop.type == ShopType.UpgradeShop and \
shop.region.name == "Capacity Upgrade":
shop.clear_inventory()
if (multiworld.worlds[player].options.shuffle_shop_inventories or multiworld.worlds[player].options.randomize_shop_prices
or multiworld.worlds[player].options.randomize_cost_types):
if (local_world.options.shuffle_shop_inventories or local_world.options.randomize_shop_prices
or local_world.options.randomize_cost_types):
shops = []
total_inventory = []
for shop in multiworld.shops:
if shop.region.player == player:
if shop.type == ShopType.Shop and not shop.locked:
shops.append(shop)
total_inventory.extend(shop.inventory)
for shop in local_world.shops:
if shop.type == ShopType.Shop and not shop.locked:
shops.append(shop)
total_inventory.extend(shop.inventory)
for item in total_inventory:
item["price_type"], item["price"] = get_price(multiworld, item, player)
if multiworld.worlds[player].options.shuffle_shop_inventories:
if local_world.options.shuffle_shop_inventories:
multiworld.random.shuffle(total_inventory)
i = 0
@@ -407,7 +408,7 @@ price_rate_display = {
}
def get_price_modifier(item):
def get_price_modifier(item) -> float:
if item.game == "A Link to the Past":
if any(x in item.name for x in
['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
@@ -418,9 +419,9 @@ def get_price_modifier(item):
elif any(x in item.name for x in ['Small Key', 'Heart']):
return 0.5
else:
return 1
return 1.0
if item.advancement:
return 1
return 1.0
elif item.useful:
return 0.5
else:
@@ -471,7 +472,7 @@ def get_price(multiworld, item, player: int, price_type=None):
def shop_price_rules(state: CollectionState, player: int, location: ALttPLocation):
if location.shop_price_type == ShopPriceType.Hearts:
return has_hearts(state, player, (location.shop_price / 8) + 1)
return has_hearts(state, player, (location.shop_price // 8) + 1)
elif location.shop_price_type == ShopPriceType.Bombs:
return can_use_bombs(state, player, location.shop_price)
elif location.shop_price_type == ShopPriceType.Arrows:

View File

@@ -14,13 +14,13 @@ def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bo
def can_buy_unlimited(state: CollectionState, item: str, player: int) -> bool:
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(state) for
shop in state.multiworld.shops)
return any(shop.has_unlimited(item) and shop.region.can_reach(state) for
shop in state.multiworld.worlds[player].shops)
def can_buy(state: CollectionState, item: str, player: int) -> bool:
return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(state) for
shop in state.multiworld.shops)
return any(shop.has(item) and shop.region.can_reach(state) for
shop in state.multiworld.worlds[player].shops)
def can_shoot_arrows(state: CollectionState, player: int, count: int = 0) -> bool:

View File

@@ -236,6 +236,8 @@ class ALTTPWorld(World):
required_client_version = (0, 4, 1)
web = ALTTPWeb()
shops: list[Shop]
pedestal_credit_texts: typing.Dict[int, str] = \
{data.item_code: data.pedestal_credit for data in item_table.values() if data.pedestal_credit}
sickkid_credit_texts: typing.Dict[int, str] = \
@@ -282,6 +284,10 @@ class ALTTPWorld(World):
clock_mode: str = ""
treasure_hunt_required: int = 0
treasure_hunt_total: int = 0
light_world_light_cone: bool = False
dark_world_light_cone: bool = False
save_and_quit_from_boss: bool = True
rupoor_cost: int = 10
def __init__(self, *args, **kwargs):
self.dungeon_local_item_names = set()
@@ -298,6 +304,7 @@ class ALTTPWorld(World):
self.fix_trock_exit = None
self.required_medallions = ["Ether", "Quake"]
self.escape_assist = []
self.shops = []
super(ALTTPWorld, self).__init__(*args, **kwargs)
@classmethod
@@ -800,7 +807,7 @@ class ALTTPWorld(World):
return shop_data
if shop_info := [build_shop_info(shop) for shop in self.multiworld.shops if shop.custom]:
if shop_info := [build_shop_info(shop) for shop in self.shops if shop.custom]:
spoiler_handle.write('\n\nShops:\n\n')
for shop_data in shop_info:
spoiler_handle.write("{} [{}]\n {}\n".format(shop_data['location'], shop_data['type'], "\n ".join(