575 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			575 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import binascii
 | 
						|
import dataclasses
 | 
						|
import os
 | 
						|
import typing
 | 
						|
import logging
 | 
						|
import re
 | 
						|
 | 
						|
import settings
 | 
						|
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Tutorial
 | 
						|
from Fill import fill_restrictive
 | 
						|
from worlds.AutoWorld import WebWorld, World
 | 
						|
from .Common import *
 | 
						|
from . import ItemIconGuessing
 | 
						|
from .Items import (DungeonItemData, DungeonItemType, ItemName, LinksAwakeningItem, TradeItemData,
 | 
						|
                    ladxr_item_to_la_item_name, links_awakening_items, links_awakening_items_by_name,
 | 
						|
                    links_awakening_item_name_groups)
 | 
						|
from .LADXR.itempool import ItemPool as LADXRItemPool
 | 
						|
from .LADXR.locations.constants import CHEST_ITEMS
 | 
						|
from .LADXR.locations.instrument import Instrument
 | 
						|
from .LADXR.logic import Logic as LADXRLogic
 | 
						|
from .LADXR.settings import Settings as LADXRSettings
 | 
						|
from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup
 | 
						|
from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion,
 | 
						|
                        create_regions_from_ladxr, get_locations_to_id,
 | 
						|
                        links_awakening_location_name_groups)
 | 
						|
from .Options import DungeonItemShuffle, ShuffleInstruments, LinksAwakeningOptions, ladx_option_groups
 | 
						|
from .Rom import LADXProcedurePatch, write_patch_data
 | 
						|
 | 
						|
DEVELOPER_MODE = False
 | 
						|
 | 
						|
 | 
						|
class LinksAwakeningSettings(settings.Group):
 | 
						|
    class RomFile(settings.UserFilePath):
 | 
						|
        """File name of the Link's Awakening DX rom"""
 | 
						|
        copy_to = "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"
 | 
						|
        description = "LADX ROM File"
 | 
						|
        md5s = [LADXProcedurePatch.hash]
 | 
						|
 | 
						|
    class RomStart(str):
 | 
						|
        """
 | 
						|
        Set this to false to never autostart a rom (such as after patching)
 | 
						|
                    true  for operating system default program
 | 
						|
        Alternatively, a path to a program to open the .gbc file with
 | 
						|
        Examples:
 | 
						|
           Retroarch:
 | 
						|
        rom_start: "C:/RetroArch-Win64/retroarch.exe -L sameboy"
 | 
						|
           BizHawk:
 | 
						|
        rom_start: "C:/BizHawk-2.9-win-x64/EmuHawk.exe --lua=data/lua/connector_ladx_bizhawk.lua"
 | 
						|
        """
 | 
						|
 | 
						|
    class DisplayMsgs(settings.Bool):
 | 
						|
        """Display message inside of Bizhawk"""
 | 
						|
 | 
						|
    class GfxModFile(settings.FilePath):
 | 
						|
        """
 | 
						|
        Gfxmod file, get it from upstream: https://github.com/daid/LADXR/tree/master/gfx
 | 
						|
        Only .bin or .bdiff files
 | 
						|
        The same directory will be checked for a matching text modification file
 | 
						|
        """
 | 
						|
 | 
						|
    rom_file: RomFile = RomFile(RomFile.copy_to)
 | 
						|
    rom_start: typing.Union[RomStart, bool] = True
 | 
						|
    gfx_mod_file: GfxModFile = GfxModFile()
 | 
						|
 | 
						|
class LinksAwakeningWebWorld(WebWorld):
 | 
						|
    tutorials = [Tutorial(
 | 
						|
        "Multiworld Setup Guide",
 | 
						|
        "A guide to setting up Links Awakening DX for MultiWorld.",
 | 
						|
        "English",
 | 
						|
        "setup_en.md",
 | 
						|
        "setup/en",
 | 
						|
        ["zig"]
 | 
						|
    )]
 | 
						|
    theme = "dirt"
 | 
						|
    option_groups = ladx_option_groups
 | 
						|
    options_presets: typing.Dict[str, typing.Dict[str, typing.Any]] = {
 | 
						|
        "Keysanity": {
 | 
						|
            "shuffle_nightmare_keys": "any_world",
 | 
						|
            "shuffle_small_keys": "any_world",
 | 
						|
            "shuffle_maps": "any_world",
 | 
						|
            "shuffle_compasses": "any_world",
 | 
						|
            "shuffle_stone_beaks": "any_world",
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
class LinksAwakeningWorld(World):
 | 
						|
    """
 | 
						|
    After a previous adventure, Link is stranded on Koholint Island, full of mystery and familiar faces.
 | 
						|
    Gather the 8 Instruments of the Sirens to wake the Wind Fish, so that Link can go home!
 | 
						|
    """
 | 
						|
    game = LINKS_AWAKENING  # name of the game/world
 | 
						|
    web = LinksAwakeningWebWorld()
 | 
						|
 | 
						|
    options_dataclass = LinksAwakeningOptions
 | 
						|
    options: LinksAwakeningOptions
 | 
						|
    settings: typing.ClassVar[LinksAwakeningSettings]
 | 
						|
    topology_present = True  # show path to required location checks in spoiler
 | 
						|
 | 
						|
    # ID of first item and location, could be hard-coded but code may be easier
 | 
						|
    # to read with this as a propery.
 | 
						|
    base_id = BASE_ID
 | 
						|
    # Instead of dynamic numbering, IDs could be part of data.
 | 
						|
 | 
						|
    # The following two dicts are required for the generation to know which
 | 
						|
    # items exist. They could be generated from json or something else. They can
 | 
						|
    # include events, but don't have to since events will be placed manually.
 | 
						|
    item_name_to_id = {
 | 
						|
        item.item_name : BASE_ID + item.item_id for item in links_awakening_items
 | 
						|
    }
 | 
						|
 | 
						|
    item_name_to_data = links_awakening_items_by_name
 | 
						|
 | 
						|
    location_name_to_id = get_locations_to_id()
 | 
						|
 | 
						|
    # Items can be grouped using their names to allow easy checking if any item
 | 
						|
    # from that group has been collected. Group names can also be used for !hint
 | 
						|
    item_name_groups = links_awakening_item_name_groups
 | 
						|
 | 
						|
    location_name_groups = links_awakening_location_name_groups
 | 
						|
 | 
						|
    prefill_dungeon_items = None
 | 
						|
 | 
						|
    ladxr_settings: LADXRSettings
 | 
						|
    ladxr_logic: LADXRLogic
 | 
						|
    ladxr_itempool: LADXRItemPool
 | 
						|
 | 
						|
    multi_key: bytearray
 | 
						|
 | 
						|
    rupees = {
 | 
						|
        ItemName.RUPEES_20: 20,
 | 
						|
        ItemName.RUPEES_50: 50,
 | 
						|
        ItemName.RUPEES_100: 100,
 | 
						|
        ItemName.RUPEES_200: 200,
 | 
						|
        ItemName.RUPEES_500: 500,
 | 
						|
    }
 | 
						|
 | 
						|
    def convert_ap_options_to_ladxr_logic(self):
 | 
						|
        self.ladxr_settings = LADXRSettings(dataclasses.asdict(self.options))
 | 
						|
 | 
						|
        self.ladxr_settings.validate()
 | 
						|
        world_setup = LADXRWorldSetup()
 | 
						|
        world_setup.randomize(self.ladxr_settings, self.random)
 | 
						|
        self.ladxr_logic = LADXRLogic(configuration_options=self.ladxr_settings, world_setup=world_setup)
 | 
						|
        self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.ladxr_settings, self.random, bool(self.options.stabilize_item_pool)).toDict()
 | 
						|
 | 
						|
 | 
						|
    def generate_early(self) -> None:
 | 
						|
        self.dungeon_item_types = {
 | 
						|
        }
 | 
						|
        for dungeon_item_type in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]:
 | 
						|
            option_name = "shuffle_" + dungeon_item_type
 | 
						|
            option: DungeonItemShuffle = getattr(self.options, option_name)
 | 
						|
 | 
						|
            self.dungeon_item_types[option.ladxr_item] = option.value
 | 
						|
 | 
						|
            # The color dungeon does not contain an instrument
 | 
						|
            num_items = 8 if dungeon_item_type == "instruments" else 9
 | 
						|
 | 
						|
            # For any and different world, set item rule instead
 | 
						|
            if option.value == DungeonItemShuffle.option_own_world:
 | 
						|
                self.options.local_items.value |= {
 | 
						|
                    ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
 | 
						|
                }
 | 
						|
            elif option.value == DungeonItemShuffle.option_different_world:
 | 
						|
                self.options.non_local_items.value |= {
 | 
						|
                    ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
 | 
						|
                }
 | 
						|
 | 
						|
    def create_regions(self) -> None:
 | 
						|
        # Initialize
 | 
						|
        self.convert_ap_options_to_ladxr_logic()
 | 
						|
        regions = create_regions_from_ladxr(self.player, self.multiworld, self.ladxr_logic)
 | 
						|
        self.multiworld.regions += regions
 | 
						|
 | 
						|
        # Connect Menu -> Start
 | 
						|
        start = None
 | 
						|
        for region in regions:
 | 
						|
            if region.name == "Start House":
 | 
						|
                start = region
 | 
						|
                break
 | 
						|
 | 
						|
        assert(start)
 | 
						|
 | 
						|
        menu_region = LinksAwakeningRegion("Menu", None, "Menu", self.player, self.multiworld)        
 | 
						|
        menu_region.exits = [Entrance(self.player, "Start Game", menu_region)]
 | 
						|
        menu_region.exits[0].connect(start)
 | 
						|
        
 | 
						|
        self.multiworld.regions.append(menu_region)
 | 
						|
 | 
						|
        # Place RAFT, other access events
 | 
						|
        for region in regions:
 | 
						|
            for loc in region.locations:
 | 
						|
                if loc.address is None:
 | 
						|
                    loc.place_locked_item(self.create_event(loc.ladxr_item.event))
 | 
						|
        
 | 
						|
        # Connect Windfish -> Victory
 | 
						|
        windfish = self.multiworld.get_region("Windfish", self.player)
 | 
						|
        l = Location(self.player, "Windfish", parent=windfish)
 | 
						|
        windfish.locations = [l]
 | 
						|
                
 | 
						|
        l.place_locked_item(self.create_event("An Alarm Clock"))
 | 
						|
        
 | 
						|
        self.multiworld.completion_condition[self.player] = lambda state: state.has("An Alarm Clock", player=self.player)
 | 
						|
 | 
						|
    def create_item(self, item_name: str):
 | 
						|
        return LinksAwakeningItem(self.item_name_to_data[item_name], self, self.player)
 | 
						|
 | 
						|
    def create_event(self, event: str):
 | 
						|
        return Item(event, ItemClassification.progression, None, self.player)
 | 
						|
 | 
						|
    def create_items(self) -> None:
 | 
						|
        itempool = []
 | 
						|
 | 
						|
        exclude = [item.name for item in self.multiworld.precollected_items[self.player]]
 | 
						|
 | 
						|
        self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ]
 | 
						|
        self.prefill_own_dungeons = []
 | 
						|
        self.pre_fill_items = []
 | 
						|
        # option_original_dungeon = 0
 | 
						|
        # option_own_dungeons = 1
 | 
						|
        # option_own_world = 2
 | 
						|
        # option_any_world = 3
 | 
						|
        # option_different_world = 4
 | 
						|
        # option_delete = 5
 | 
						|
 | 
						|
        for ladx_item_name, count in self.ladxr_itempool.items():
 | 
						|
            # event
 | 
						|
            if ladx_item_name not in ladxr_item_to_la_item_name:
 | 
						|
                continue
 | 
						|
            item_name = ladxr_item_to_la_item_name[ladx_item_name]
 | 
						|
            for _ in range(count):
 | 
						|
                if item_name in exclude:
 | 
						|
                    exclude.remove(item_name)  # this is destructive. create unique list above
 | 
						|
                    self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
 | 
						|
                else:
 | 
						|
                    item = self.create_item(item_name)
 | 
						|
 | 
						|
                    if not self.options.tradequest and isinstance(item.item_data, TradeItemData):
 | 
						|
                        location = self.multiworld.get_location(item.item_data.vanilla_location, self.player)
 | 
						|
                        location.place_locked_item(item)
 | 
						|
                        location.show_in_spoiler = False
 | 
						|
                        continue
 | 
						|
 | 
						|
                    if isinstance(item.item_data, DungeonItemData):
 | 
						|
                        item_type = item.item_data.ladxr_id[:-1]
 | 
						|
                        shuffle_type = self.dungeon_item_types[item_type]
 | 
						|
 | 
						|
                        if item.item_data.dungeon_item_type == DungeonItemType.INSTRUMENT and shuffle_type == ShuffleInstruments.option_vanilla:
 | 
						|
                            # Find instrument, lock
 | 
						|
                            # TODO: we should be able to pinpoint the region we want, save a lookup table please
 | 
						|
                            found = False
 | 
						|
                            for r in self.multiworld.get_regions(self.player):
 | 
						|
                                if r.dungeon_index != item.item_data.dungeon_index:
 | 
						|
                                    continue
 | 
						|
                                for loc in r.locations:
 | 
						|
                                    if not isinstance(loc, LinksAwakeningLocation):
 | 
						|
                                        continue
 | 
						|
                                    if not isinstance(loc.ladxr_item, Instrument):
 | 
						|
                                        continue
 | 
						|
                                    loc.place_locked_item(item)
 | 
						|
                                    found = True
 | 
						|
                                    break
 | 
						|
                                if found:
 | 
						|
                                    break
 | 
						|
                        else:
 | 
						|
                            if shuffle_type == DungeonItemShuffle.option_original_dungeon:
 | 
						|
                                self.prefill_original_dungeon[item.item_data.dungeon_index - 1].append(item)
 | 
						|
                                self.pre_fill_items.append(item)
 | 
						|
                            elif shuffle_type == DungeonItemShuffle.option_own_dungeons:
 | 
						|
                                self.prefill_own_dungeons.append(item)
 | 
						|
                                self.pre_fill_items.append(item)
 | 
						|
                            else:
 | 
						|
                                itempool.append(item)
 | 
						|
                    else:
 | 
						|
                        itempool.append(item)
 | 
						|
 | 
						|
        self.multi_key = self.generate_multi_key()
 | 
						|
 | 
						|
        # Add special case for trendy shop access
 | 
						|
        trendy_region = self.multiworld.get_region("Trendy Shop", self.player)
 | 
						|
        event_location = Location(self.player, "Can Play Trendy Game", parent=trendy_region)
 | 
						|
        trendy_region.locations.insert(0, event_location)
 | 
						|
        event_location.place_locked_item(self.create_event("Can Play Trendy Game"))
 | 
						|
       
 | 
						|
        self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []]     
 | 
						|
        for r in self.multiworld.get_regions(self.player):
 | 
						|
            # Set aside dungeon locations
 | 
						|
            if r.dungeon_index:
 | 
						|
                self.dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations
 | 
						|
                for location in r.locations:
 | 
						|
                    # Don't place dungeon items on pit button chest, to reduce chance of the filler blowing up
 | 
						|
                    # TODO: no need for this if small key shuffle
 | 
						|
                    if location.name == "Pit Button Chest (Tail Cave)" or location.item:
 | 
						|
                        self.dungeon_locations_by_dungeon[r.dungeon_index - 1].remove(location)
 | 
						|
                    # Properly fill locations within dungeon
 | 
						|
                    location.dungeon = r.dungeon_index
 | 
						|
 | 
						|
        if self.options.tarins_gift != "any_item":
 | 
						|
            self.force_start_item(itempool)
 | 
						|
 | 
						|
 | 
						|
        self.multiworld.itempool += itempool
 | 
						|
 | 
						|
    def force_start_item(self, itempool):
 | 
						|
        start_loc = self.multiworld.get_location("Tarin's Gift (Mabe Village)", self.player)
 | 
						|
        if not start_loc.item:
 | 
						|
            """
 | 
						|
            Find an item that forces progression or a bush breaker for the player, depending on settings.
 | 
						|
            """
 | 
						|
            def is_possible_start_item(item):
 | 
						|
                return item.advancement and item.name not in self.options.non_local_items
 | 
						|
 | 
						|
            def opens_new_regions(item):
 | 
						|
                collection_state = base_collection_state.copy()
 | 
						|
                collection_state.collect(item, prevent_sweep=True)
 | 
						|
                collection_state.sweep_for_advancements(self.get_locations())
 | 
						|
                return len(collection_state.reachable_regions[self.player]) > reachable_count
 | 
						|
 | 
						|
            start_items = [item for item in itempool if is_possible_start_item(item)]
 | 
						|
            self.random.shuffle(start_items)
 | 
						|
 | 
						|
            if self.options.tarins_gift == "bush_breaker":
 | 
						|
                start_item = next((item for item in start_items if item.name in links_awakening_item_name_groups["Bush Breakers"]), None)
 | 
						|
 | 
						|
            else:  # local_progression
 | 
						|
                entrance_mapping = self.ladxr_logic.world_setup.entrance_mapping
 | 
						|
                # Tail key opens a region but not a location if d1 entrance is not mapped to d1 or d4
 | 
						|
                # exclude it in these cases to avoid fill errors
 | 
						|
                if entrance_mapping['d1'] not in ['d1', 'd4']:
 | 
						|
                    start_items = [item for item in start_items if item.name != 'Tail Key']
 | 
						|
                # Exclude shovel unless starting in Mabe Village
 | 
						|
                if entrance_mapping['start_house'] not in ['start_house', 'shop']:
 | 
						|
                    start_items = [item for item in start_items if item.name != 'Shovel']
 | 
						|
                base_collection_state = CollectionState(self.multiworld)
 | 
						|
                base_collection_state.sweep_for_advancements(self.get_locations())
 | 
						|
                reachable_count = len(base_collection_state.reachable_regions[self.player])
 | 
						|
                start_item = next((item for item in start_items if opens_new_regions(item)), None)
 | 
						|
 | 
						|
            if start_item:
 | 
						|
                # Make sure we're removing the same copy of the item that we're placing
 | 
						|
                # (.remove checks __eq__, which could be a different copy, so we find the first index and use .pop)
 | 
						|
                start_item = itempool.pop(itempool.index(start_item))
 | 
						|
                start_loc.place_locked_item(start_item)
 | 
						|
            else:
 | 
						|
                logging.getLogger("Link's Awakening Logger").warning(f"No {self.options.tarins_gift.current_option_name} available for Tarin's Gift.")
 | 
						|
 | 
						|
 | 
						|
    def get_pre_fill_items(self):
 | 
						|
        return self.pre_fill_items
 | 
						|
 | 
						|
    def pre_fill(self) -> None:
 | 
						|
        allowed_locations_by_item = {}
 | 
						|
 | 
						|
 | 
						|
        # Set up filter rules
 | 
						|
 | 
						|
        # set containing the list of all possible dungeon locations for the player
 | 
						|
        all_dungeon_locs = set()
 | 
						|
        
 | 
						|
        # Do dungeon specific things
 | 
						|
        for dungeon_index in range(0, 9):
 | 
						|
            # set up allow-list for dungeon specific items
 | 
						|
            locs = set(loc for loc in self.dungeon_locations_by_dungeon[dungeon_index] if not loc.item)
 | 
						|
            for item in self.prefill_original_dungeon[dungeon_index]:
 | 
						|
                allowed_locations_by_item[item] = locs
 | 
						|
 | 
						|
            # ...and gather the list of all dungeon locations
 | 
						|
            all_dungeon_locs |= locs
 | 
						|
            # ...also set the rules for the dungeon
 | 
						|
            for location in locs:
 | 
						|
                orig_rule = location.item_rule
 | 
						|
                # If an item is about to be placed on a dungeon location, it can go there iff 
 | 
						|
                # 1. it fits the general rules for that location (probably 'return True' for most places)
 | 
						|
                # 2. Either
 | 
						|
                #    2a. it's not a restricted dungeon item
 | 
						|
                #    2b. it's a restricted dungeon item and this location is specified as allowed
 | 
						|
                location.item_rule = lambda item, location=location, orig_rule=orig_rule: \
 | 
						|
                    (item not in allowed_locations_by_item or location in allowed_locations_by_item[item]) and orig_rule(item)
 | 
						|
 | 
						|
        # Now set up the allow-list for any-dungeon items
 | 
						|
        for item in self.prefill_own_dungeons:
 | 
						|
            # They of course get to go in any spot
 | 
						|
            allowed_locations_by_item[item] = all_dungeon_locs
 | 
						|
 | 
						|
        # Get the list of locations and shuffle
 | 
						|
        all_dungeon_locs_to_fill = sorted(all_dungeon_locs)
 | 
						|
 | 
						|
        self.random.shuffle(all_dungeon_locs_to_fill)
 | 
						|
 | 
						|
        # Get the list of items and sort by priority
 | 
						|
        def priority(item):
 | 
						|
            # 0 - Nightmare dungeon-specific
 | 
						|
            # 1 - Key dungeon-specific
 | 
						|
            # 2 - Other dungeon-specific
 | 
						|
            # 3 - Nightmare any local dungeon
 | 
						|
            # 4 - Key any local dungeon
 | 
						|
            # 5 - Other any local dungeon
 | 
						|
            i = 2
 | 
						|
            if "Nightmare" in item.name:
 | 
						|
                i = 0
 | 
						|
            elif "Key" in item.name:
 | 
						|
                i = 1
 | 
						|
            if allowed_locations_by_item[item] is all_dungeon_locs:
 | 
						|
                i += 3
 | 
						|
            return i
 | 
						|
        all_dungeon_items_to_fill = self.get_pre_fill_items()
 | 
						|
        all_dungeon_items_to_fill.sort(key=priority)
 | 
						|
 | 
						|
        # Set up state
 | 
						|
        partial_all_state = CollectionState(self.multiworld)
 | 
						|
        # Collect every item from the item pool and every pre-fill item like MultiWorld.get_all_state, except not our own pre-fill items.
 | 
						|
        for item in self.multiworld.itempool:
 | 
						|
            partial_all_state.collect(item, prevent_sweep=True)
 | 
						|
        for player in self.multiworld.player_ids:
 | 
						|
            if player == self.player:
 | 
						|
                # Don't collect the items we're about to place.
 | 
						|
                continue
 | 
						|
            subworld = self.multiworld.worlds[player]
 | 
						|
            for item in subworld.get_pre_fill_items():
 | 
						|
                partial_all_state.collect(item, prevent_sweep=True)
 | 
						|
 | 
						|
        # Sweep to pick up already placed items that are reachable with everything but the dungeon items.
 | 
						|
        partial_all_state.sweep_for_advancements()
 | 
						|
 | 
						|
        fill_restrictive(self.multiworld, partial_all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, single_player_placement=True, allow_partial=False)
 | 
						|
        
 | 
						|
 | 
						|
    name_cache = {}
 | 
						|
    # Tries to associate an icon from another game with an icon we have
 | 
						|
    def guess_icon_for_other_world(self, foreign_item):
 | 
						|
        if not self.name_cache:
 | 
						|
            for item in ladxr_item_to_la_item_name.keys():
 | 
						|
                self.name_cache[item] = item
 | 
						|
                splits = item.split("_")
 | 
						|
                for word in item.split("_"):
 | 
						|
                    if word not in ItemIconGuessing.BLOCKED_ASSOCIATIONS and not word.isnumeric():
 | 
						|
                        self.name_cache[word] = item
 | 
						|
            for name in ItemIconGuessing.SYNONYMS.values():
 | 
						|
                assert name in self.name_cache, name
 | 
						|
                assert name in CHEST_ITEMS, name
 | 
						|
            self.name_cache.update(ItemIconGuessing.SYNONYMS)
 | 
						|
            pluralizations = {k + "S": v for k, v in self.name_cache.items()}
 | 
						|
            self.name_cache = pluralizations | self.name_cache
 | 
						|
 | 
						|
        uppered = foreign_item.name.upper()
 | 
						|
        foreign_game = self.multiworld.game[foreign_item.player]
 | 
						|
        phrases = ItemIconGuessing.PHRASES.copy()
 | 
						|
        if foreign_game in ItemIconGuessing.GAME_SPECIFIC_PHRASES:
 | 
						|
            phrases.update(ItemIconGuessing.GAME_SPECIFIC_PHRASES[foreign_game])
 | 
						|
 | 
						|
        for phrase, icon in phrases.items():
 | 
						|
            if phrase.upper() in uppered:
 | 
						|
                return icon
 | 
						|
        # pattern for breaking down camelCase, also separates out digits
 | 
						|
        pattern = re.compile(r"(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-zA-Z])(?=\d)")
 | 
						|
        possibles = pattern.sub(' ', foreign_item.name).upper()
 | 
						|
        for ch in "[]()_":
 | 
						|
            possibles = possibles.replace(ch, " ")
 | 
						|
        possibles = possibles.split()
 | 
						|
        for name in possibles:
 | 
						|
            if name in self.name_cache:
 | 
						|
                return self.name_cache[name]
 | 
						|
        
 | 
						|
        return "TRADING_ITEM_LETTER"
 | 
						|
 | 
						|
    def generate_output(self, output_directory: str):
 | 
						|
        # copy items back to locations
 | 
						|
        for r in self.multiworld.get_regions(self.player):
 | 
						|
            for loc in r.locations:
 | 
						|
                if isinstance(loc, LinksAwakeningLocation):
 | 
						|
                    assert(loc.item)
 | 
						|
                        
 | 
						|
                    # If we're a links awakening item, just use the item
 | 
						|
                    if isinstance(loc.item, LinksAwakeningItem):
 | 
						|
                        loc.ladxr_item.item = loc.item.item_data.ladxr_id
 | 
						|
 | 
						|
                    # If the item name contains "sword", use a sword icon, etc
 | 
						|
                    # Otherwise, use a cute letter as the icon
 | 
						|
                    elif self.options.foreign_item_icons == 'guess_by_name':
 | 
						|
                        loc.ladxr_item.item = self.guess_icon_for_other_world(loc.item)
 | 
						|
                        loc.ladxr_item.setCustomItemName(loc.item.name)
 | 
						|
 | 
						|
                    else:
 | 
						|
                        if loc.item.advancement:
 | 
						|
                            loc.ladxr_item.item = 'PIECE_OF_POWER'
 | 
						|
                        else:
 | 
						|
                            loc.ladxr_item.item = 'GUARDIAN_ACORN'
 | 
						|
                        loc.ladxr_item.custom_item_name = loc.item.name
 | 
						|
 | 
						|
                    if loc.item:
 | 
						|
                        loc.ladxr_item.item_owner = loc.item.player
 | 
						|
                    else:
 | 
						|
                        loc.ladxr_item.item_owner = self.player
 | 
						|
 | 
						|
                    # Kind of kludge, make it possible for the location to differentiate between local and remote items
 | 
						|
                    loc.ladxr_item.location_owner = self.player
 | 
						|
 | 
						|
        
 | 
						|
        patch = LADXProcedurePatch(player=self.player, player_name=self.player_name)
 | 
						|
        write_patch_data(self, patch)
 | 
						|
        out_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}"
 | 
						|
                                                  f"{patch.patch_file_ending}")
 | 
						|
 | 
						|
        patch.write(out_path)
 | 
						|
 | 
						|
    def generate_multi_key(self):
 | 
						|
        return bytearray(self.random.getrandbits(8) for _ in range(10)) + self.player.to_bytes(2, 'big')
 | 
						|
 | 
						|
    def modify_multidata(self, multidata: dict):
 | 
						|
        multidata["connect_names"][binascii.hexlify(self.multi_key).decode()] = multidata["connect_names"][self.player_name]
 | 
						|
 | 
						|
    def collect(self, state, item: Item) -> bool:
 | 
						|
        change = super().collect(state, item)
 | 
						|
        if change and item.name in self.rupees:
 | 
						|
            state.prog_items[self.player]["RUPEES"] += self.rupees[item.name]
 | 
						|
        return change
 | 
						|
 | 
						|
    def remove(self, state, item: Item) -> bool:
 | 
						|
        change = super().remove(state, item)
 | 
						|
        if change and item.name in self.rupees:
 | 
						|
            state.prog_items[self.player]["RUPEES"] -= self.rupees[item.name]
 | 
						|
        return change
 | 
						|
 | 
						|
    # Same fill choices and weights used in LADXR.itempool.__randomizeRupees
 | 
						|
    filler_choices = ("Bomb", "Single Arrow", "10 Arrows", "Magic Powder", "Medicine")
 | 
						|
    filler_weights = ( 10,     5,              10,          10,             1)
 | 
						|
 | 
						|
    def get_filler_item_name(self) -> str:
 | 
						|
        if self.options.stabilize_item_pool:
 | 
						|
            return "Nothing"
 | 
						|
        return self.random.choices(self.filler_choices, self.filler_weights)[0]
 | 
						|
 | 
						|
    def fill_slot_data(self):
 | 
						|
        slot_data = {}
 | 
						|
 | 
						|
        if not self.multiworld.is_race:
 | 
						|
            # all of these option are NOT used by the LADX- or Text-Client.
 | 
						|
            # they are used by Magpie tracker (https://github.com/kbranch/Magpie/wiki/Autotracker-API)
 | 
						|
            # for convenient auto-tracking of the generated settings and adjusting the tracker accordingly
 | 
						|
 | 
						|
            slot_options = ["instrument_count"]
 | 
						|
 | 
						|
            slot_options_display_name = [
 | 
						|
                "goal",
 | 
						|
                "logic",
 | 
						|
                "tradequest",
 | 
						|
                "rooster",
 | 
						|
                "experimental_dungeon_shuffle",
 | 
						|
                "experimental_entrance_shuffle",
 | 
						|
                "trendy_game",
 | 
						|
                "gfxmod",
 | 
						|
                "shuffle_nightmare_keys",
 | 
						|
                "shuffle_small_keys",
 | 
						|
                "shuffle_maps",
 | 
						|
                "shuffle_compasses",
 | 
						|
                "shuffle_stone_beaks",
 | 
						|
                "shuffle_instruments",
 | 
						|
                "nag_messages",
 | 
						|
                "hard_mode",
 | 
						|
                "overworld",
 | 
						|
            ]
 | 
						|
 | 
						|
            # use the default behaviour to grab options
 | 
						|
            slot_data = self.options.as_dict(*slot_options)
 | 
						|
 | 
						|
            # for options which should not get the internal int value but the display name use the extra handling
 | 
						|
            slot_data.update({
 | 
						|
                option: value.current_key
 | 
						|
                for option, value in dataclasses.asdict(self.options).items() if option in slot_options_display_name
 | 
						|
            })
 | 
						|
 | 
						|
            slot_data.update({"entrance_mapping": self.ladxr_logic.world_setup.entrance_mapping})
 | 
						|
 | 
						|
        return slot_data
 |