1059 lines
		
	
	
		
			45 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			1059 lines
		
	
	
		
			45 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 
								 | 
							
								import io
							 | 
						||
| 
								 | 
							
								import hashlib
							 | 
						||
| 
								 | 
							
								import logging
							 | 
						||
| 
								 | 
							
								import os
							 | 
						||
| 
								 | 
							
								import struct
							 | 
						||
| 
								 | 
							
								import random
							 | 
						||
| 
								 | 
							
								from collections import OrderedDict
							 | 
						||
| 
								 | 
							
								import urllib.request
							 | 
						||
| 
								 | 
							
								from urllib.error import URLError, HTTPError
							 | 
						||
| 
								 | 
							
								import json
							 | 
						||
| 
								 | 
							
								from enum import Enum
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								from .HintList import getHint, getHintGroup, Hint, hintExclusions
							 | 
						||
| 
								 | 
							
								from .Messages import update_message_by_id
							 | 
						||
| 
								 | 
							
								from .TextBox import line_wrap
							 | 
						||
| 
								 | 
							
								from .Utils import data_path, read_json
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								bingoBottlesForHints = (
							 | 
						||
| 
								 | 
							
								    "Bottle", "Bottle with Red Potion","Bottle with Green Potion", "Bottle with Blue Potion",
							 | 
						||
| 
								 | 
							
								    "Bottle with Fairy", "Bottle with Fish", "Bottle with Blue Fire", "Bottle with Bugs",
							 | 
						||
| 
								 | 
							
								    "Bottle with Big Poe", "Bottle with Poe",
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								defaultHintDists = [
							 | 
						||
| 
								 | 
							
								    'balanced.json', 'bingo.json', 'ddr.json', 'scrubs.json', 'strong.json', 'tournament.json', 'useless.json', 'very_strong.json'
							 | 
						||
| 
								 | 
							
								]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class RegionRestriction(Enum):
							 | 
						||
| 
								 | 
							
								    NONE = 0,
							 | 
						||
| 
								 | 
							
								    DUNGEON = 1,
							 | 
						||
| 
								 | 
							
								    OVERWORLD = 2,
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class GossipStone():
							 | 
						||
| 
								 | 
							
								    def __init__(self, name, location):
							 | 
						||
| 
								 | 
							
								        self.name = name
							 | 
						||
| 
								 | 
							
								        self.location = location
							 | 
						||
| 
								 | 
							
								        self.reachable = True
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class GossipText():
							 | 
						||
| 
								 | 
							
								    def __init__(self, text, colors=None, prefix="They say that "):
							 | 
						||
| 
								 | 
							
								        text = prefix + text
							 | 
						||
| 
								 | 
							
								        text = text[:1].upper() + text[1:]
							 | 
						||
| 
								 | 
							
								        self.text = text
							 | 
						||
| 
								 | 
							
								        self.colors = colors
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def to_json(self):
							 | 
						||
| 
								 | 
							
								        return {'text': self.text, 'colors': self.colors}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def __str__(self):
							 | 
						||
| 
								 | 
							
								        return get_raw_text(line_wrap(colorText(self)))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#   Abbreviations
							 | 
						||
| 
								 | 
							
								#       DMC     Death Mountain Crater
							 | 
						||
| 
								 | 
							
								#       DMT     Death Mountain Trail
							 | 
						||
| 
								 | 
							
								#       GC      Goron City
							 | 
						||
| 
								 | 
							
								#       GV      Gerudo Valley
							 | 
						||
| 
								 | 
							
								#       HC      Hyrule Castle
							 | 
						||
| 
								 | 
							
								#       HF      Hyrule Field
							 | 
						||
| 
								 | 
							
								#       KF      Kokiri Forest
							 | 
						||
| 
								 | 
							
								#       LH      Lake Hylia
							 | 
						||
| 
								 | 
							
								#       LW      Lost Woods
							 | 
						||
| 
								 | 
							
								#       SFM     Sacred Forest Meadow
							 | 
						||
| 
								 | 
							
								#       ToT     Temple of Time
							 | 
						||
| 
								 | 
							
								#       ZD      Zora's Domain
							 | 
						||
| 
								 | 
							
								#       ZF      Zora's Fountain
							 | 
						||
| 
								 | 
							
								#       ZR      Zora's River
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								gossipLocations = {
							 | 
						||
| 
								 | 
							
								    0x0405: GossipStone('DMC (Bombable Wall)',              'DMC Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0404: GossipStone('DMT (Biggoron)',                   'DMT Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x041A: GossipStone('Colossus (Spirit Temple)',         'Colossus Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0414: GossipStone('Dodongos Cavern (Bombable Wall)',  'Dodongos Cavern Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0411: GossipStone('GV (Waterfall)',                   'GV Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0415: GossipStone('GC (Maze)',                        'GC Maze Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0419: GossipStone('GC (Medigoron)',                   'GC Medigoron Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x040A: GossipStone('Graveyard (Shadow Temple)',        'Graveyard Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0412: GossipStone('HC (Malon)',                       'HC Malon Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x040B: GossipStone('HC (Rock Wall)',                   'HC Rock Wall Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0413: GossipStone('HC (Storms Grotto)',               'HC Storms Grotto Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x041F: GossipStone('KF (Deku Tree Left)',              'KF Deku Tree Gossip Stone (Left)'),
							 | 
						||
| 
								 | 
							
								    0x0420: GossipStone('KF (Deku Tree Right)',             'KF Deku Tree Gossip Stone (Right)'),
							 | 
						||
| 
								 | 
							
								    0x041E: GossipStone('KF (Outside Storms)',              'KF Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0403: GossipStone('LH (Lab)',                         'LH Lab Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x040F: GossipStone('LH (Southeast Corner)',            'LH Gossip Stone (Southeast)'),
							 | 
						||
| 
								 | 
							
								    0x0408: GossipStone('LH (Southwest Corner)',            'LH Gossip Stone (Southwest)'),
							 | 
						||
| 
								 | 
							
								    0x041D: GossipStone('LW (Bridge)',                      'LW Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0416: GossipStone('SFM (Maze Lower)',                 'SFM Maze Gossip Stone (Lower)'),
							 | 
						||
| 
								 | 
							
								    0x0417: GossipStone('SFM (Maze Upper)',                 'SFM Maze Gossip Stone (Upper)'),
							 | 
						||
| 
								 | 
							
								    0x041C: GossipStone('SFM (Saria)',                      'SFM Saria Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0406: GossipStone('ToT (Left)',                       'ToT Gossip Stone (Left)'),
							 | 
						||
| 
								 | 
							
								    0x0407: GossipStone('ToT (Left-Center)',                'ToT Gossip Stone (Left-Center)'),
							 | 
						||
| 
								 | 
							
								    0x0410: GossipStone('ToT (Right)',                      'ToT Gossip Stone (Right)'),
							 | 
						||
| 
								 | 
							
								    0x040E: GossipStone('ToT (Right-Center)',               'ToT Gossip Stone (Right-Center)'),
							 | 
						||
| 
								 | 
							
								    0x0409: GossipStone('ZD (Mweep)',                       'ZD Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0401: GossipStone('ZF (Fairy)',                       'ZF Fairy Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0402: GossipStone('ZF (Jabu)',                        'ZF Jabu Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x040D: GossipStone('ZR (Near Grottos)',                'ZR Near Grottos Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x040C: GossipStone('ZR (Near Domain)',                 'ZR Near Domain Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x041B: GossipStone('HF (Cow Grotto)',                  'HF Cow Grotto Gossip Stone'),
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    0x0430: GossipStone('HF (Near Market Grotto)',          'HF Near Market Grotto Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0432: GossipStone('HF (Southeast Grotto)',            'HF Southeast Grotto Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0433: GossipStone('HF (Open Grotto)',                 'HF Open Grotto Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0438: GossipStone('Kak (Open Grotto)',                'Kak Open Grotto Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0439: GossipStone('ZR (Open Grotto)',                 'ZR Open Grotto Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x043C: GossipStone('KF (Storms Grotto)',               'KF Storms Grotto Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0444: GossipStone('LW (Near Shortcuts Grotto)',       'LW Near Shortcuts Grotto Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x0447: GossipStone('DMT (Storms Grotto)',              'DMT Storms Grotto Gossip Stone'),
							 | 
						||
| 
								 | 
							
								    0x044A: GossipStone('DMC (Upper Grotto)',               'DMC Upper Grotto Gossip Stone'),
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								gossipLocations_reversemap = {
							 | 
						||
| 
								 | 
							
								    stone.name : stone_id for stone_id, stone in gossipLocations.items()
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def getItemGenericName(item):
							 | 
						||
| 
								 | 
							
								    if item.game != "Ocarina of Time":
							 | 
						||
| 
								 | 
							
								        return item.name
							 | 
						||
| 
								 | 
							
								    elif item.dungeonitem:
							 | 
						||
| 
								 | 
							
								        return item.type
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        return item.name
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def isRestrictedDungeonItem(dungeon, item):
							 | 
						||
| 
								 | 
							
								    if (item.map or item.compass) and dungeon.world.shuffle_mapcompass == 'dungeon':
							 | 
						||
| 
								 | 
							
								        return item in dungeon.dungeon_items
							 | 
						||
| 
								 | 
							
								    if item.type == 'SmallKey' and dungeon.world.shuffle_smallkeys == 'dungeon':
							 | 
						||
| 
								 | 
							
								        return item in dungeon.small_keys
							 | 
						||
| 
								 | 
							
								    if item.type == 'BossKey' and dungeon.world.shuffle_bosskeys == 'dungeon':
							 | 
						||
| 
								 | 
							
								        return item in dungeon.boss_key
							 | 
						||
| 
								 | 
							
								    if item.type == 'GanonBossKey' and dungeon.world.shuffle_ganon_bosskey == 'dungeon':
							 | 
						||
| 
								 | 
							
								        return item in dungeon.boss_key
							 | 
						||
| 
								 | 
							
								    return False
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Attach a player name to the item or location text.
							 | 
						||
| 
								 | 
							
								# If the associated player of the item/location and the world are the same, does nothing.
							 | 
						||
| 
								 | 
							
								# Otherwise, attaches the object's player's name to the string.
							 | 
						||
| 
								 | 
							
								def attach_name(text, hinted_object, world):
							 | 
						||
| 
								 | 
							
								    if hinted_object.player == world.player:
							 | 
						||
| 
								 | 
							
								        return text
							 | 
						||
| 
								 | 
							
								    return f"{text} for {world.world.get_player_name(hinted_object.player)}"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def add_hint(world, groups, gossip_text, count, location=None, force_reachable=False):
							 | 
						||
| 
								 | 
							
								    world.hint_rng.shuffle(groups)
							 | 
						||
| 
								 | 
							
								    skipped_groups = []
							 | 
						||
| 
								 | 
							
								    duplicates = []
							 | 
						||
| 
								 | 
							
								    first = True
							 | 
						||
| 
								 | 
							
								    success = True
							 | 
						||
| 
								 | 
							
								    # early failure if not enough
							 | 
						||
| 
								 | 
							
								    if len(groups) < int(count):
							 | 
						||
| 
								 | 
							
								        return False
							 | 
						||
| 
								 | 
							
								    # Randomly round up, if we have enough groups left
							 | 
						||
| 
								 | 
							
								    total = int(world.hint_rng.random() + count) if len(groups) > count else int(count)
							 | 
						||
| 
								 | 
							
								    while total:
							 | 
						||
| 
								 | 
							
								        if groups:
							 | 
						||
| 
								 | 
							
								            group = groups.pop(0)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            if any(map(lambda id: gossipLocations[id].reachable, group)):
							 | 
						||
| 
								 | 
							
								                stone_names = [gossipLocations[id].location for id in group]
							 | 
						||
| 
								 | 
							
								                # stone_locations = [world.get_location(stone_name) for stone_name in stone_names]
							 | 
						||
| 
								 | 
							
								                # Taking out all checks on gossip stone reachability and hint logic
							 | 
						||
| 
								 | 
							
								                if not first or True: # or any(map(lambda stone_location: can_reach_hint(worlds, stone_location, location), stone_locations)):
							 | 
						||
| 
								 | 
							
								                    # if first and location:
							 | 
						||
| 
								 | 
							
								                    #     # just name the event item after the gossip stone directly
							 | 
						||
| 
								 | 
							
								                    #     event_item = None
							 | 
						||
| 
								 | 
							
								                    #     for i, stone_name in enumerate(stone_names):
							 | 
						||
| 
								 | 
							
								                    #         # place the same event item in each location in the group
							 | 
						||
| 
								 | 
							
								                    #         if event_item is None:
							 | 
						||
| 
								 | 
							
								                    #             event_item = MakeEventItem(stone_name, stone_locations[i], event_item)
							 | 
						||
| 
								 | 
							
								                    #         else:
							 | 
						||
| 
								 | 
							
								                    #             MakeEventItem(stone_name, stone_locations[i], event_item)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                    #     # This mostly guarantees that we don't lock the player out of an item hint
							 | 
						||
| 
								 | 
							
								                    #     # by establishing a (hint -> item) -> hint -> item -> (first hint) loop
							 | 
						||
| 
								 | 
							
								                    #     location.add_rule(world.parser.parse_rule(repr(event_item.name)))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                    total -= 1
							 | 
						||
| 
								 | 
							
								                    first = False
							 | 
						||
| 
								 | 
							
								                    for id in group:
							 | 
						||
| 
								 | 
							
								                        world.gossip_hints[id] = gossip_text
							 | 
						||
| 
								 | 
							
								                    # Immediately start choosing duplicates from stones we passed up earlier
							 | 
						||
| 
								 | 
							
								                    while duplicates and total:
							 | 
						||
| 
								 | 
							
								                        group = duplicates.pop(0)
							 | 
						||
| 
								 | 
							
								                        total -= 1
							 | 
						||
| 
								 | 
							
								                        for id in group:
							 | 
						||
| 
								 | 
							
								                            world.gossip_hints[id] = gossip_text
							 | 
						||
| 
								 | 
							
								                else:
							 | 
						||
| 
								 | 
							
								                    # Temporarily skip this stone but consider it for duplicates
							 | 
						||
| 
								 | 
							
								                    duplicates.append(group)
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                if not force_reachable:
							 | 
						||
| 
								 | 
							
								                    # The stones are not readable at all in logic, so we ignore any kind of logic here
							 | 
						||
| 
								 | 
							
								                    if not first:
							 | 
						||
| 
								 | 
							
								                        total -= 1
							 | 
						||
| 
								 | 
							
								                        for id in group:
							 | 
						||
| 
								 | 
							
								                            world.gossip_hints[id] = gossip_text
							 | 
						||
| 
								 | 
							
								                    else:
							 | 
						||
| 
								 | 
							
								                        # Temporarily skip this stone but consider it for duplicates
							 | 
						||
| 
								 | 
							
								                        duplicates.append(group)
							 | 
						||
| 
								 | 
							
								                else:
							 | 
						||
| 
								 | 
							
								                    # If flagged to guarantee reachable, then skip
							 | 
						||
| 
								 | 
							
								                    # If no stones are reachable, then this will place nothing
							 | 
						||
| 
								 | 
							
								                    skipped_groups.append(group)
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            # Out of groups
							 | 
						||
| 
								 | 
							
								            if not force_reachable and len(duplicates) >= total:
							 | 
						||
| 
								 | 
							
								                # Didn't find any appropriate stones for this hint, but maybe enough completely unreachable ones.
							 | 
						||
| 
								 | 
							
								                # We'd rather not use reachable stones for this.
							 | 
						||
| 
								 | 
							
								                unr = [group for group in duplicates if all(map(lambda id: not gossipLocations[id].reachable, group))]
							 | 
						||
| 
								 | 
							
								                if len(unr) >= total:
							 | 
						||
| 
								 | 
							
								                    duplicates = [group for group in duplicates if group not in unr[:total]]
							 | 
						||
| 
								 | 
							
								                    for group in unr[:total]:
							 | 
						||
| 
								 | 
							
								                        for id in group:
							 | 
						||
| 
								 | 
							
								                            world.gossip_hints[id] = gossip_text
							 | 
						||
| 
								 | 
							
								                    # Success
							 | 
						||
| 
								 | 
							
								                    break
							 | 
						||
| 
								 | 
							
								            # Failure
							 | 
						||
| 
								 | 
							
								            success = False
							 | 
						||
| 
								 | 
							
								            break
							 | 
						||
| 
								 | 
							
								    groups.extend(duplicates)
							 | 
						||
| 
								 | 
							
								    groups.extend(skipped_groups)
							 | 
						||
| 
								 | 
							
								    return success
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def writeGossipStoneHints(world, messages):
							 | 
						||
| 
								 | 
							
								    for id, gossip_text in world.gossip_hints.items():
							 | 
						||
| 
								 | 
							
								        update_message_by_id(messages, id, str(gossip_text), 0x23)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def filterTrailingSpace(text):
							 | 
						||
| 
								 | 
							
								    if text.endswith('& '):
							 | 
						||
| 
								 | 
							
								        return text[:-1]
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        return text
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								hintPrefixes = [
							 | 
						||
| 
								 | 
							
								    'a few ',
							 | 
						||
| 
								 | 
							
								    'some ',
							 | 
						||
| 
								 | 
							
								    'plenty of ',
							 | 
						||
| 
								 | 
							
								    'a ',
							 | 
						||
| 
								 | 
							
								    'an ',
							 | 
						||
| 
								 | 
							
								    'the ',
							 | 
						||
| 
								 | 
							
								    '',
							 | 
						||
| 
								 | 
							
								]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def getSimpleHintNoPrefix(item):
							 | 
						||
| 
								 | 
							
								    hint = getHint(item.name, True).text
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    for prefix in hintPrefixes:
							 | 
						||
| 
								 | 
							
								        if hint.startswith(prefix):
							 | 
						||
| 
								 | 
							
								            # return without the prefix
							 | 
						||
| 
								 | 
							
								            return hint[len(prefix):]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # no prefex
							 | 
						||
| 
								 | 
							
								    return hint
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def colorText(gossip_text):
							 | 
						||
| 
								 | 
							
								    colorMap = {
							 | 
						||
| 
								 | 
							
								        'White':      '\x40',
							 | 
						||
| 
								 | 
							
								        'Red':        '\x41',
							 | 
						||
| 
								 | 
							
								        'Green':      '\x42',
							 | 
						||
| 
								 | 
							
								        'Blue':       '\x43',
							 | 
						||
| 
								 | 
							
								        'Light Blue': '\x44',
							 | 
						||
| 
								 | 
							
								        'Pink':       '\x45',
							 | 
						||
| 
								 | 
							
								        'Yellow':     '\x46',
							 | 
						||
| 
								 | 
							
								        'Black':      '\x47',
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    text = gossip_text.text
							 | 
						||
| 
								 | 
							
								    colors = list(gossip_text.colors) if gossip_text.colors is not None else []
							 | 
						||
| 
								 | 
							
								    color = 'White'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    while '#' in text:
							 | 
						||
| 
								 | 
							
								        splitText = text.split('#', 2)
							 | 
						||
| 
								 | 
							
								        if len(colors) > 0:
							 | 
						||
| 
								 | 
							
								            color = colors.pop()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        for prefix in hintPrefixes:
							 | 
						||
| 
								 | 
							
								            if splitText[1].startswith(prefix):
							 | 
						||
| 
								 | 
							
								                splitText[0] += splitText[1][:len(prefix)]
							 | 
						||
| 
								 | 
							
								                splitText[1] = splitText[1][len(prefix):]
							 | 
						||
| 
								 | 
							
								                break
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        splitText[1] = '\x05' + colorMap[color] + splitText[1] + '\x05\x40'
							 | 
						||
| 
								 | 
							
								        text = ''.join(splitText)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return text
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class HintAreaNotFound(RuntimeError):
							 | 
						||
| 
								 | 
							
								    pass
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Peforms a breadth first search to find the closest hint area from a given spot (location or entrance)
							 | 
						||
| 
								 | 
							
								# May fail to find a hint if the given spot is only accessible from the root and not from any other region with a hint area
							 | 
						||
| 
								 | 
							
								# Returns the name of the location if the spot is not in OoT
							 | 
						||
| 
								 | 
							
								def get_hint_area(spot):
							 | 
						||
| 
								 | 
							
								    if spot.game == 'Ocarina of Time':
							 | 
						||
| 
								 | 
							
								        already_checked = []
							 | 
						||
| 
								 | 
							
								        spot_queue = [spot]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        while spot_queue:
							 | 
						||
| 
								 | 
							
								            current_spot = spot_queue.pop(0)
							 | 
						||
| 
								 | 
							
								            already_checked.append(current_spot)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            parent_region = current_spot.parent_region
							 | 
						||
| 
								 | 
							
								        
							 | 
						||
| 
								 | 
							
								            if parent_region.dungeon:
							 | 
						||
| 
								 | 
							
								                return parent_region.dungeon.hint_text
							 | 
						||
| 
								 | 
							
								            elif parent_region.hint_text and (spot.parent_region.name == 'Root' or parent_region.name != 'Root'):
							 | 
						||
| 
								 | 
							
								                return parent_region.hint_text
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            spot_queue.extend(list(filter(lambda ent: ent not in already_checked, parent_region.entrances)))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        raise HintAreaNotFound('No hint area could be found for %s [World %d]' % (spot, spot.world.id))
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        return spot.name
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_woth_hint(world, checked):
							 | 
						||
| 
								 | 
							
								    locations = world.required_locations
							 | 
						||
| 
								 | 
							
								    locations = list(filter(lambda location:
							 | 
						||
| 
								 | 
							
								        location.name not in checked[location.player]
							 | 
						||
| 
								 | 
							
								        and not (world.woth_dungeon >= world.hint_dist_user['dungeons_woth_limit'] and location.parent_region.dungeon)
							 | 
						||
| 
								 | 
							
								        and location.name not in world.hint_exclusions
							 | 
						||
| 
								 | 
							
								        and location.name not in world.hint_type_overrides['woth']
							 | 
						||
| 
								 | 
							
								        and location.item.name not in world.item_hint_type_overrides['woth'],
							 | 
						||
| 
								 | 
							
								        locations))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if not locations:
							 | 
						||
| 
								 | 
							
								        return None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    location = world.hint_rng.choice(locations)
							 | 
						||
| 
								 | 
							
								    checked[location.player].add(location.name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if location.parent_region.dungeon:
							 | 
						||
| 
								 | 
							
								        world.woth_dungeon += 1
							 | 
						||
| 
								 | 
							
								        location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        location_text = get_hint_area(location)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if world.triforce_hunt:
							 | 
						||
| 
								 | 
							
								        return (GossipText('#%s# is on the path of gold.' % location_text, ['Light Blue']), location)
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        return (GossipText('#%s# is on the way of the hero.' % location_text, ['Light Blue']), location)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_barren_hint(world, checked):
							 | 
						||
| 
								 | 
							
								    if not hasattr(world, 'get_barren_hint_prev'):
							 | 
						||
| 
								 | 
							
								        world.get_barren_hint_prev = RegionRestriction.NONE
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    areas = list(filter(lambda area:
							 | 
						||
| 
								 | 
							
								        area not in checked[world.player]
							 | 
						||
| 
								 | 
							
								        and area not in world.hint_type_overrides['barren']
							 | 
						||
| 
								 | 
							
								        and not (world.barren_dungeon >= world.hint_dist_user['dungeons_barren_limit'] and world.empty_areas[area]['dungeon']),
							 | 
						||
| 
								 | 
							
								        world.empty_areas.keys()))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if not areas:
							 | 
						||
| 
								 | 
							
								        return None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Randomly choose between overworld or dungeon
							 | 
						||
| 
								 | 
							
								    dungeon_areas = list(filter(lambda area: world.empty_areas[area]['dungeon'], areas))
							 | 
						||
| 
								 | 
							
								    overworld_areas = list(filter(lambda area: not world.empty_areas[area]['dungeon'], areas))
							 | 
						||
| 
								 | 
							
								    if not dungeon_areas:
							 | 
						||
| 
								 | 
							
								        # no dungeons left, default to overworld
							 | 
						||
| 
								 | 
							
								        world.get_barren_hint_prev = RegionRestriction.OVERWORLD
							 | 
						||
| 
								 | 
							
								    elif not overworld_areas:
							 | 
						||
| 
								 | 
							
								        # no overworld left, default to dungeons
							 | 
						||
| 
								 | 
							
								        world.get_barren_hint_prev = RegionRestriction.DUNGEON
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        if world.get_barren_hint_prev == RegionRestriction.NONE:
							 | 
						||
| 
								 | 
							
								            # 50/50 draw on the first hint
							 | 
						||
| 
								 | 
							
								            world.get_barren_hint_prev = world.hint_rng.choices([RegionRestriction.DUNGEON, RegionRestriction.OVERWORLD], [0.5, 0.5])[0]
							 | 
						||
| 
								 | 
							
								        elif world.get_barren_hint_prev == RegionRestriction.DUNGEON:
							 | 
						||
| 
								 | 
							
								            # weights 75% against drawing dungeon again
							 | 
						||
| 
								 | 
							
								            world.get_barren_hint_prev = world.hint_rng.choices([RegionRestriction.DUNGEON, RegionRestriction.OVERWORLD], [0.25, 0.75])[0]
							 | 
						||
| 
								 | 
							
								        elif world.get_barren_hint_prev == RegionRestriction.OVERWORLD:
							 | 
						||
| 
								 | 
							
								            # weights 75% against drawing overworld again
							 | 
						||
| 
								 | 
							
								            world.get_barren_hint_prev = world.hint_rng.choices([RegionRestriction.DUNGEON, RegionRestriction.OVERWORLD], [0.75, 0.25])[0]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if world.get_barren_hint_prev == RegionRestriction.DUNGEON:
							 | 
						||
| 
								 | 
							
								        areas = dungeon_areas
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        areas = overworld_areas
							 | 
						||
| 
								 | 
							
								    if not areas:
							 | 
						||
| 
								 | 
							
								        return None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    area_weights = [world.empty_areas[area]['weight'] for area in areas]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    area = world.hint_rng.choices(areas, weights=area_weights)[0]
							 | 
						||
| 
								 | 
							
								    if world.empty_areas[area]['dungeon']:
							 | 
						||
| 
								 | 
							
								        world.barren_dungeon += 1
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    checked[world.player].add(area)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return (GossipText("plundering #%s# is a foolish choice." % area, ['Pink']), None)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def is_not_checked(location, checked):
							 | 
						||
| 
								 | 
							
								    return not (location.name in checked[location.player] or get_hint_area(location) in checked)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_good_item_hint(world, checked):
							 | 
						||
| 
								 | 
							
								    locations = list(filter(lambda location:
							 | 
						||
| 
								 | 
							
								        is_not_checked(location, checked)
							 | 
						||
| 
								 | 
							
								        and not location.locked
							 | 
						||
| 
								 | 
							
								        and location.name not in world.hint_exclusions
							 | 
						||
| 
								 | 
							
								        and location.name not in world.hint_type_overrides['item']
							 | 
						||
| 
								 | 
							
								        and location.item.name not in world.item_hint_type_overrides['item'],
							 | 
						||
| 
								 | 
							
								        world.major_item_locations))
							 | 
						||
| 
								 | 
							
								    if not locations:
							 | 
						||
| 
								 | 
							
								        return None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    location = world.hint_rng.choice(locations)
							 | 
						||
| 
								 | 
							
								    checked[location.player].add(location.name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								    if location.parent_region.dungeon:
							 | 
						||
| 
								 | 
							
								        location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								        return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), 
							 | 
						||
| 
								 | 
							
								            ['Green', 'Red']), location)
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        location_text = get_hint_area(location)
							 | 
						||
| 
								 | 
							
								        return (GossipText('#%s# can be found at #%s#.' % (attach_name(item_text, location.item, world), attach_name(location_text, location, world)), 
							 | 
						||
| 
								 | 
							
								            ['Red', 'Green']), location)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_specific_item_hint(world, checked):
							 | 
						||
| 
								 | 
							
								    if len(world.named_item_pool) == 0:
							 | 
						||
| 
								 | 
							
								        logger = logging.getLogger('')
							 | 
						||
| 
								 | 
							
								        logger.info("Named item hint requested, but pool is empty.")
							 | 
						||
| 
								 | 
							
								        return None  
							 | 
						||
| 
								 | 
							
								    while True:
							 | 
						||
| 
								 | 
							
								        itemname = world.named_item_pool.pop(0)
							 | 
						||
| 
								 | 
							
								        if itemname == "Bottle" and world.hint_dist == "bingo":
							 | 
						||
| 
								 | 
							
								            locations = [
							 | 
						||
| 
								 | 
							
								                location for location in world.world.get_filled_locations()
							 | 
						||
| 
								 | 
							
								                if (is_not_checked(location, checked)
							 | 
						||
| 
								 | 
							
								                    and location.name not in world.hint_exclusions
							 | 
						||
| 
								 | 
							
								                    and location.item.name in bingoBottlesForHints
							 | 
						||
| 
								 | 
							
								                    and not location.locked
							 | 
						||
| 
								 | 
							
								                    and location.name not in world.hint_type_overrides['named-item'])
							 | 
						||
| 
								 | 
							
								            ]
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            locations = [
							 | 
						||
| 
								 | 
							
								                location for location in world.world.get_filled_locations()
							 | 
						||
| 
								 | 
							
								                if (is_not_checked(location, checked)
							 | 
						||
| 
								 | 
							
								                    and location.name not in world.hint_exclusions
							 | 
						||
| 
								 | 
							
								                    and location.item.name == itemname
							 | 
						||
| 
								 | 
							
								                    and not location.locked
							 | 
						||
| 
								 | 
							
								                    and location.name not in world.hint_type_overrides['named-item'])
							 | 
						||
| 
								 | 
							
								            ]
							 | 
						||
| 
								 | 
							
								        if len(locations) > 0:
							 | 
						||
| 
								 | 
							
								            break
							 | 
						||
| 
								 | 
							
								        if len(world.named_item_pool) == 0:
							 | 
						||
| 
								 | 
							
								            return None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    location = world.hint_rng.choice(locations)
							 | 
						||
| 
								 | 
							
								    checked[location.player].add(location.name)
							 | 
						||
| 
								 | 
							
								    item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    if location.parent_region.dungeon:
							 | 
						||
| 
								 | 
							
								        location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								        if world.hint_dist_user.get('vague_named_items', False):
							 | 
						||
| 
								 | 
							
								            return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location)
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), 
							 | 
						||
| 
								 | 
							
								                ['Green', 'Red']), location)
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        location_text = get_hint_area(location)
							 | 
						||
| 
								 | 
							
								        if world.hint_dist_user.get('vague_named_items', False):
							 | 
						||
| 
								 | 
							
								            return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location)
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            return (GossipText('#%s# can be found at #%s#.' % (attach_name(item_text, location.item, world), attach_name(location_text, location, world)), 
							 | 
						||
| 
								 | 
							
								                ['Red', 'Green']), location)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_random_location_hint(world, checked):
							 | 
						||
| 
								 | 
							
								    locations = list(filter(lambda location:
							 | 
						||
| 
								 | 
							
								        is_not_checked(location, checked)
							 | 
						||
| 
								 | 
							
								        and location.item.type not in ('Drop', 'Event', 'Shop', 'DungeonReward')
							 | 
						||
| 
								 | 
							
								        # and not (location.parent_region.dungeon and isRestrictedDungeonItem(location.parent_region.dungeon, location.item)) # AP already locks dungeon items
							 | 
						||
| 
								 | 
							
								        and not location.locked
							 | 
						||
| 
								 | 
							
								        and location.name not in world.hint_exclusions
							 | 
						||
| 
								 | 
							
								        and location.name not in world.hint_type_overrides['item']
							 | 
						||
| 
								 | 
							
								        and location.item.name not in world.item_hint_type_overrides['item'],
							 | 
						||
| 
								 | 
							
								        world.world.get_filled_locations(world.player)))
							 | 
						||
| 
								 | 
							
								    if not locations:
							 | 
						||
| 
								 | 
							
								        return None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    location = world.hint_rng.choice(locations)
							 | 
						||
| 
								 | 
							
								    checked[location.player].add(location.name)
							 | 
						||
| 
								 | 
							
								    dungeon = location.parent_region.dungeon
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								    if dungeon:
							 | 
						||
| 
								 | 
							
								        location_text = getHint(dungeon.name, world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								        return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), 
							 | 
						||
| 
								 | 
							
								            ['Green', 'Red']), location)
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        location_text = get_hint_area(location)
							 | 
						||
| 
								 | 
							
								        return (GossipText('#%s# can be found at #%s#.' % (attach_name(item_text, location.item, world), attach_name(location_text, location, world)), 
							 | 
						||
| 
								 | 
							
								            ['Red', 'Green']), location)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_specific_hint(world, checked, type):
							 | 
						||
| 
								 | 
							
								    hintGroup = getHintGroup(type, world)
							 | 
						||
| 
								 | 
							
								    hintGroup = list(filter(lambda hint: is_not_checked(world.get_location(hint.name), checked), hintGroup))
							 | 
						||
| 
								 | 
							
								    if not hintGroup:
							 | 
						||
| 
								 | 
							
								        return None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    hint = world.hint_rng.choice(hintGroup)
							 | 
						||
| 
								 | 
							
								    location = world.get_location(hint.name)
							 | 
						||
| 
								 | 
							
								    checked[location.player].add(location.name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if location.name in world.hint_text_overrides:
							 | 
						||
| 
								 | 
							
								        location_text = world.hint_text_overrides[location.name]
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        location_text = hint.text
							 | 
						||
| 
								 | 
							
								    if '#' not in location_text:
							 | 
						||
| 
								 | 
							
								        location_text = '#%s#' % location_text
							 | 
						||
| 
								 | 
							
								    item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return (GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), 
							 | 
						||
| 
								 | 
							
								        ['Green', 'Red']), location)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_sometimes_hint(world, checked):
							 | 
						||
| 
								 | 
							
								    return get_specific_hint(world, checked, 'sometimes')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_song_hint(world, checked):
							 | 
						||
| 
								 | 
							
								    return get_specific_hint(world, checked, 'song')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_overworld_hint(world, checked):
							 | 
						||
| 
								 | 
							
								    return get_specific_hint(world, checked, 'overworld')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_dungeon_hint(world, checked):
							 | 
						||
| 
								 | 
							
								    return get_specific_hint(world, checked, 'dungeon')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# probably broken
							 | 
						||
| 
								 | 
							
								def get_entrance_hint(world, checked):
							 | 
						||
| 
								 | 
							
								    if not world.entrance_shuffle:
							 | 
						||
| 
								 | 
							
								        return None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    entrance_hints = list(filter(lambda hint: hint.name not in checked[world.player], getHintGroup('entrance', world)))
							 | 
						||
| 
								 | 
							
								    shuffled_entrance_hints = list(filter(lambda entrance_hint: world.get_entrance(entrance_hint.name).shuffled, entrance_hints))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    regions_with_hint = [hint.name for hint in getHintGroup('region', world)]
							 | 
						||
| 
								 | 
							
								    valid_entrance_hints = list(filter(lambda entrance_hint:
							 | 
						||
| 
								 | 
							
								                                       (world.get_entrance(entrance_hint.name).connected_region.name in regions_with_hint or
							 | 
						||
| 
								 | 
							
								                                        world.get_entrance(entrance_hint.name).connected_region.dungeon), shuffled_entrance_hints))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if not valid_entrance_hints:
							 | 
						||
| 
								 | 
							
								        return None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    entrance_hint = world.hint_rng.choice(valid_entrance_hints)
							 | 
						||
| 
								 | 
							
								    entrance = world.get_entrance(entrance_hint.name)
							 | 
						||
| 
								 | 
							
								    checked[world.player].add(entrance.name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    entrance_text = entrance_hint.text
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if '#' not in entrance_text:
							 | 
						||
| 
								 | 
							
								        entrance_text = '#%s#' % entrance_text
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    connected_region = entrance.connected_region
							 | 
						||
| 
								 | 
							
								    if connected_region.dungeon:
							 | 
						||
| 
								 | 
							
								        region_text = getHint(connected_region.dungeon.name, world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        region_text = getHint(connected_region.name, world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if '#' not in region_text:
							 | 
						||
| 
								 | 
							
								        region_text = '#%s#' % region_text
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return (GossipText('%s %s.' % (entrance_text, region_text), ['Light Blue', 'Green']), None)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_junk_hint(world, checked):
							 | 
						||
| 
								 | 
							
								    hints = getHintGroup('junk', world)
							 | 
						||
| 
								 | 
							
								    hints = list(filter(lambda hint: hint.name not in checked[world.player], hints))
							 | 
						||
| 
								 | 
							
								    if not hints:
							 | 
						||
| 
								 | 
							
								        return None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    hint = world.hint_rng.choice(hints)
							 | 
						||
| 
								 | 
							
								    checked[world.player].add(hint.name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return (GossipText(hint.text, prefix=''), None)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								hint_func = {
							 | 
						||
| 
								 | 
							
								    'trial':      lambda world, checked: None,
							 | 
						||
| 
								 | 
							
								    'always':     lambda world, checked: None,
							 | 
						||
| 
								 | 
							
								    'woth':             get_woth_hint,
							 | 
						||
| 
								 | 
							
								    'barren':           get_barren_hint,
							 | 
						||
| 
								 | 
							
								    'item':             get_good_item_hint,
							 | 
						||
| 
								 | 
							
								    'sometimes':        get_sometimes_hint,
							 | 
						||
| 
								 | 
							
								    'song':             get_song_hint,
							 | 
						||
| 
								 | 
							
								    'overworld':        get_overworld_hint,
							 | 
						||
| 
								 | 
							
								    'dungeon':          get_dungeon_hint,
							 | 
						||
| 
								 | 
							
								    'entrance':         get_entrance_hint,
							 | 
						||
| 
								 | 
							
								    'random':           get_random_location_hint,
							 | 
						||
| 
								 | 
							
								    'junk':             get_junk_hint,
							 | 
						||
| 
								 | 
							
								    'named-item':       get_specific_item_hint
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								hint_dist_keys = {
							 | 
						||
| 
								 | 
							
								    'trial',
							 | 
						||
| 
								 | 
							
								    'always',
							 | 
						||
| 
								 | 
							
								    'woth',
							 | 
						||
| 
								 | 
							
								    'barren',
							 | 
						||
| 
								 | 
							
								    'item',
							 | 
						||
| 
								 | 
							
								    'song',
							 | 
						||
| 
								 | 
							
								    'overworld',
							 | 
						||
| 
								 | 
							
								    'dungeon',
							 | 
						||
| 
								 | 
							
								    'entrance',
							 | 
						||
| 
								 | 
							
								    'sometimes',
							 | 
						||
| 
								 | 
							
								    'random',
							 | 
						||
| 
								 | 
							
								    'junk',
							 | 
						||
| 
								 | 
							
								    'named-item'
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# builds out general hints based on location and whether an item is required or not
							 | 
						||
| 
								 | 
							
								def buildWorldGossipHints(world, checkedLocations=None):
							 | 
						||
| 
								 | 
							
								    # Seed the RNG
							 | 
						||
| 
								 | 
							
								    world.hint_rng = world.world.slot_seeds[world.player]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # rebuild hint exclusion list
							 | 
						||
| 
								 | 
							
								    hintExclusions(world, clear_cache=True)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    world.barren_dungeon = 0
							 | 
						||
| 
								 | 
							
								    world.woth_dungeon = 0
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if checkedLocations is None:
							 | 
						||
| 
								 | 
							
								        checkedLocations = {player: set() for player in world.world.player_ids}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # If Ganondorf can be reached without Light Arrows, add to checkedLocations to prevent extra hinting
							 | 
						||
| 
								 | 
							
								    # Can only be forced with vanilla bridge
							 | 
						||
| 
								 | 
							
								    if world.bridge != 'vanilla':
							 | 
						||
| 
								 | 
							
								        try:
							 | 
						||
| 
								 | 
							
								            light_arrow_location = world.world.find_item("Light Arrows", world.player)
							 | 
						||
| 
								 | 
							
								            checkedLocations[light_arrow_location.player].add(light_arrow_location.name)
							 | 
						||
| 
								 | 
							
								        except StopIteration: # start with them
							 | 
						||
| 
								 | 
							
								            pass
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    stoneIDs = list(gossipLocations.keys())
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if 'disabled' in world.hint_dist_user:
							 | 
						||
| 
								 | 
							
								        for stone_name in world.hint_dist_user['disabled']:
							 | 
						||
| 
								 | 
							
								            try:
							 | 
						||
| 
								 | 
							
								                stone_id = gossipLocations_reversemap[stone_name]
							 | 
						||
| 
								 | 
							
								            except KeyError:
							 | 
						||
| 
								 | 
							
								                raise ValueError(f'Gossip stone location "{stone_name}" is not valid')
							 | 
						||
| 
								 | 
							
								            stoneIDs.remove(stone_id)
							 | 
						||
| 
								 | 
							
								            (gossip_text, _) = get_junk_hint(world, checkedLocations)
							 | 
						||
| 
								 | 
							
								            world.gossip_hints[stone_id] = gossip_text
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    stoneGroups = []
							 | 
						||
| 
								 | 
							
								    if 'groups' in world.hint_dist_user:
							 | 
						||
| 
								 | 
							
								        for group_names in world.hint_dist_user['groups']:
							 | 
						||
| 
								 | 
							
								            group = []
							 | 
						||
| 
								 | 
							
								            for stone_name in group_names:
							 | 
						||
| 
								 | 
							
								                try:
							 | 
						||
| 
								 | 
							
								                    stone_id = gossipLocations_reversemap[stone_name]
							 | 
						||
| 
								 | 
							
								                except KeyError:
							 | 
						||
| 
								 | 
							
								                    raise ValueError(f'Gossip stone location "{stone_name}" is not valid')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                stoneIDs.remove(stone_id)
							 | 
						||
| 
								 | 
							
								                group.append(stone_id)
							 | 
						||
| 
								 | 
							
								            stoneGroups.append(group)
							 | 
						||
| 
								 | 
							
								    # put the remaining locations into singleton groups
							 | 
						||
| 
								 | 
							
								    stoneGroups.extend([[id] for id in stoneIDs])
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    world.hint_rng.shuffle(stoneGroups)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Load hint distro from distribution file or pre-defined settings
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # 'fixed' key is used to mimic the tournament distribution, creating a list of fixed hint types to fill
							 | 
						||
| 
								 | 
							
								    # Once the fixed hint type list is exhausted, weighted random choices are taken like all non-tournament sets
							 | 
						||
| 
								 | 
							
								    # This diverges from the tournament distribution where leftover stones are filled with sometimes hints (or random if no sometimes locations remain to be hinted)
							 | 
						||
| 
								 | 
							
								    sorted_dist = {}
							 | 
						||
| 
								 | 
							
								    type_count = 1
							 | 
						||
| 
								 | 
							
								    hint_dist = OrderedDict({})
							 | 
						||
| 
								 | 
							
								    fixed_hint_types = []
							 | 
						||
| 
								 | 
							
								    max_order = 0
							 | 
						||
| 
								 | 
							
								    for hint_type in world.hint_dist_user['distribution']:
							 | 
						||
| 
								 | 
							
								        if world.hint_dist_user['distribution'][hint_type]['order'] > 0:
							 | 
						||
| 
								 | 
							
								            hint_order = int(world.hint_dist_user['distribution'][hint_type]['order'])
							 | 
						||
| 
								 | 
							
								            sorted_dist[hint_order] = hint_type
							 | 
						||
| 
								 | 
							
								            if max_order < hint_order:
							 | 
						||
| 
								 | 
							
								                max_order = hint_order
							 | 
						||
| 
								 | 
							
								            type_count = type_count + 1
							 | 
						||
| 
								 | 
							
								    if (type_count - 1) < max_order:
							 | 
						||
| 
								 | 
							
								        raise Exception("There are gaps in the custom hint orders. Please revise your plando file to remove them.")
							 | 
						||
| 
								 | 
							
								    for i in range(1, type_count):
							 | 
						||
| 
								 | 
							
								        hint_type = sorted_dist[i]
							 | 
						||
| 
								 | 
							
								        if world.hint_dist_user['distribution'][hint_type]['copies'] > 0:
							 | 
						||
| 
								 | 
							
								            fixed_num = world.hint_dist_user['distribution'][hint_type]['fixed']
							 | 
						||
| 
								 | 
							
								            hint_weight = world.hint_dist_user['distribution'][hint_type]['weight']
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            fixed_num = 0
							 | 
						||
| 
								 | 
							
								            hint_weight = 0
							 | 
						||
| 
								 | 
							
								        hint_dist[hint_type] = (hint_weight, world.hint_dist_user['distribution'][hint_type]['copies'])
							 | 
						||
| 
								 | 
							
								        hint_dist.move_to_end(hint_type)
							 | 
						||
| 
								 | 
							
								        fixed_hint_types.extend([hint_type] * int(fixed_num))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    hint_types, hint_prob = zip(*hint_dist.items())
							 | 
						||
| 
								 | 
							
								    hint_prob, _ = zip(*hint_prob)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Add required location hints, only if hint copies > 0
							 | 
						||
| 
								 | 
							
								    if hint_dist['always'][1] > 0:
							 | 
						||
| 
								 | 
							
								        alwaysLocations = getHintGroup('always', world)
							 | 
						||
| 
								 | 
							
								        for hint in alwaysLocations:
							 | 
						||
| 
								 | 
							
								            location = world.get_location(hint.name)
							 | 
						||
| 
								 | 
							
								            checkedLocations[location.player].add(hint.name)
							 | 
						||
| 
								 | 
							
								            if location.item.name in bingoBottlesForHints and world.hint_dist == 'bingo':
							 | 
						||
| 
								 | 
							
								                always_item = 'Bottle'
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                always_item = location.item.name
							 | 
						||
| 
								 | 
							
								            if always_item in world.named_item_pool:
							 | 
						||
| 
								 | 
							
								                world.named_item_pool.remove(always_item)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            if location.name in world.hint_text_overrides:
							 | 
						||
| 
								 | 
							
								                location_text = world.hint_text_overrides[location.name]
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                location_text = getHint(location.name, world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								            if '#' not in location_text:
							 | 
						||
| 
								 | 
							
								                location_text = '#%s#' % location_text
							 | 
						||
| 
								 | 
							
								            item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								            add_hint(world, stoneGroups, GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), 
							 | 
						||
| 
								 | 
							
								                ['Green', 'Red']), hint_dist['always'][1], location, force_reachable=True)
							 | 
						||
| 
								 | 
							
								            logging.getLogger('').debug('Placed always hint for %s.', location.name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Add trial hints, only if hint copies > 0
							 | 
						||
| 
								 | 
							
								    if hint_dist['trial'][1] > 0:
							 | 
						||
| 
								 | 
							
								        if world.trials == 6:
							 | 
						||
| 
								 | 
							
								            add_hint(world, stoneGroups, GossipText("#Ganon's Tower# is protected by a powerful barrier.", ['Pink']), hint_dist['trial'][1], force_reachable=True)
							 | 
						||
| 
								 | 
							
								        elif world.trials == 0:
							 | 
						||
| 
								 | 
							
								            add_hint(world, stoneGroups, GossipText("Sheik dispelled the barrier around #Ganon's Tower#.", ['Yellow']), hint_dist['trial'][1], force_reachable=True)
							 | 
						||
| 
								 | 
							
								        elif world.trials < 6 and world.trials > 3:
							 | 
						||
| 
								 | 
							
								            for trial,skipped in world.skipped_trials.items():
							 | 
						||
| 
								 | 
							
								                if skipped:
							 | 
						||
| 
								 | 
							
								                    add_hint(world, stoneGroups,GossipText("the #%s Trial# was dispelled by Sheik." % trial, ['Yellow']), hint_dist['trial'][1], force_reachable=True)
							 | 
						||
| 
								 | 
							
								        elif world.trials <= 3 and world.trials > 0:
							 | 
						||
| 
								 | 
							
								            for trial,skipped in world.skipped_trials.items():
							 | 
						||
| 
								 | 
							
								                if not skipped:
							 | 
						||
| 
								 | 
							
								                    add_hint(world, stoneGroups, GossipText("the #%s Trial# protects Ganon's Tower." % trial, ['Pink']), hint_dist['trial'][1], force_reachable=True)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Add user-specified hinted item locations if using a built-in hint distribution
							 | 
						||
| 
								 | 
							
								    # Raise error if hint copies is zero
							 | 
						||
| 
								 | 
							
								    if len(world.named_item_pool) > 0 and world.hint_dist_user['named_items_required']:
							 | 
						||
| 
								 | 
							
								        if hint_dist['named-item'][1] == 0:
							 | 
						||
| 
								 | 
							
								            raise Exception('User-provided item hints were requested, but copies per named-item hint is zero')
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            for i in range(0, len(world.named_item_pool)):
							 | 
						||
| 
								 | 
							
								                hint = get_specific_item_hint(world, checkedLocations)
							 | 
						||
| 
								 | 
							
								                if hint == None:
							 | 
						||
| 
								 | 
							
								                    raise Exception('No valid hints for user-provided item')
							 | 
						||
| 
								 | 
							
								                else:
							 | 
						||
| 
								 | 
							
								                    gossip_text, location = hint
							 | 
						||
| 
								 | 
							
								                    place_ok = add_hint(world, stoneGroups, gossip_text, hint_dist['named-item'][1], location)
							 | 
						||
| 
								 | 
							
								                    if not place_ok:
							 | 
						||
| 
								 | 
							
								                        raise Exception('Not enough gossip stones for user-provided item hints')
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    # Shuffle named items hints
							 | 
						||
| 
								 | 
							
								    # When all items are not required to be hinted, this allows for
							 | 
						||
| 
								 | 
							
								    # opportunity-style hints to be drawn at random from the defined list.
							 | 
						||
| 
								 | 
							
								    world.hint_rng.shuffle(world.named_item_pool)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    hint_types = list(hint_types)
							 | 
						||
| 
								 | 
							
								    hint_prob  = list(hint_prob)
							 | 
						||
| 
								 | 
							
								    hint_counts = {}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    custom_fixed = True
							 | 
						||
| 
								 | 
							
								    while stoneGroups:
							 | 
						||
| 
								 | 
							
								        if fixed_hint_types:
							 | 
						||
| 
								 | 
							
								            hint_type = fixed_hint_types.pop(0)
							 | 
						||
| 
								 | 
							
								            copies = hint_dist[hint_type][1]
							 | 
						||
| 
								 | 
							
								            if copies > len(stoneGroups):
							 | 
						||
| 
								 | 
							
								                # Quiet to avoid leaking information.
							 | 
						||
| 
								 | 
							
								                logging.getLogger('').debug(f'Not enough gossip stone locations ({len(stoneGroups)} groups) for fixed hint type {hint_type} with {copies} copies, proceeding with available stones.')
							 | 
						||
| 
								 | 
							
								                copies = len(stoneGroups)
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            custom_fixed = False
							 | 
						||
| 
								 | 
							
								            # Make sure there are enough stones left for each hint type
							 | 
						||
| 
								 | 
							
								            num_types = len(hint_types)
							 | 
						||
| 
								 | 
							
								            hint_types = list(filter(lambda htype: hint_dist[htype][1] <= len(stoneGroups), hint_types))
							 | 
						||
| 
								 | 
							
								            new_num_types = len(hint_types)
							 | 
						||
| 
								 | 
							
								            if new_num_types == 0:
							 | 
						||
| 
								 | 
							
								                raise Exception('Not enough gossip stone locations for remaining weighted hint types.')
							 | 
						||
| 
								 | 
							
								            elif new_num_types < num_types:
							 | 
						||
| 
								 | 
							
								                hint_prob = []
							 | 
						||
| 
								 | 
							
								                for htype in hint_types:
							 | 
						||
| 
								 | 
							
								                    hint_prob.append(hint_dist[htype][0])
							 | 
						||
| 
								 | 
							
								            try:
							 | 
						||
| 
								 | 
							
								                # Weight the probabilities such that hints that are over the expected proportion
							 | 
						||
| 
								 | 
							
								                # will be drawn less, and hints that are under will be drawn more.
							 | 
						||
| 
								 | 
							
								                # This tightens the variance quite a bit. The variance can be adjusted via the power
							 | 
						||
| 
								 | 
							
								                weighted_hint_prob = []
							 | 
						||
| 
								 | 
							
								                for w1_type, w1_prob in zip(hint_types, hint_prob):
							 | 
						||
| 
								 | 
							
								                    p = w1_prob
							 | 
						||
| 
								 | 
							
								                    if p != 0: # If the base prob is 0, then it's 0
							 | 
						||
| 
								 | 
							
								                        for w2_type, w2_prob in zip(hint_types, hint_prob):
							 | 
						||
| 
								 | 
							
								                            if w2_prob != 0: # If the other prob is 0, then it has no effect
							 | 
						||
| 
								 | 
							
								                                # Raising this term to a power greater than 1 will decrease variance
							 | 
						||
| 
								 | 
							
								                                # Conversely, a power less than 1 will increase variance
							 | 
						||
| 
								 | 
							
								                                p = p * (((hint_counts.get(w2_type, 0) / w2_prob) + 1) / ((hint_counts.get(w1_type, 0) / w1_prob) + 1))
							 | 
						||
| 
								 | 
							
								                    weighted_hint_prob.append(p)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                hint_type = world.hint_rng.choices(hint_types, weights=weighted_hint_prob)[0]
							 | 
						||
| 
								 | 
							
								                copies = hint_dist[hint_type][1]
							 | 
						||
| 
								 | 
							
								            except IndexError:
							 | 
						||
| 
								 | 
							
								                raise Exception('Not enough valid hints to fill gossip stone locations.')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        hint = hint_func[hint_type](world, checkedLocations)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if hint == None:
							 | 
						||
| 
								 | 
							
								            index = hint_types.index(hint_type)
							 | 
						||
| 
								 | 
							
								            hint_prob[index] = 0
							 | 
						||
| 
								 | 
							
								            # Zero out the probability in the base distribution in case the probability list is modified
							 | 
						||
| 
								 | 
							
								            # to fit hint types in remaining gossip stones
							 | 
						||
| 
								 | 
							
								            hint_dist[hint_type] = (0.0, copies)
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            gossip_text, location = hint
							 | 
						||
| 
								 | 
							
								            place_ok = add_hint(world, stoneGroups, gossip_text, copies, location)
							 | 
						||
| 
								 | 
							
								            if place_ok:
							 | 
						||
| 
								 | 
							
								                hint_counts[hint_type] = hint_counts.get(hint_type, 0) + 1
							 | 
						||
| 
								 | 
							
								                if location is None:
							 | 
						||
| 
								 | 
							
								                    logging.getLogger('').debug('Placed %s hint.', hint_type)
							 | 
						||
| 
								 | 
							
								                else:
							 | 
						||
| 
								 | 
							
								                    logging.getLogger('').debug('Placed %s hint for %s.', hint_type, location.name)
							 | 
						||
| 
								 | 
							
								            if not place_ok and custom_fixed:
							 | 
						||
| 
								 | 
							
								                logging.getLogger('').debug('Failed to place %s fixed hint for %s.', hint_type, location.name)
							 | 
						||
| 
								 | 
							
								                fixed_hint_types.insert(0, hint_type)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# builds text that is displayed at the temple of time altar for child and adult, rewards pulled based off of item in a fixed order.
							 | 
						||
| 
								 | 
							
								def buildAltarHints(world, messages, include_rewards=True, include_wincons=True):
							 | 
						||
| 
								 | 
							
								    # text that appears at altar as a child.
							 | 
						||
| 
								 | 
							
								    child_text = '\x08'
							 | 
						||
| 
								 | 
							
								    if include_rewards:
							 | 
						||
| 
								 | 
							
								        bossRewardsSpiritualStones = [
							 | 
						||
| 
								 | 
							
								            ('Kokiri Emerald',   'Green'), 
							 | 
						||
| 
								 | 
							
								            ('Goron Ruby',       'Red'), 
							 | 
						||
| 
								 | 
							
								            ('Zora Sapphire',    'Blue'),
							 | 
						||
| 
								 | 
							
								        ]
							 | 
						||
| 
								 | 
							
								        child_text += getHint('Spiritual Stone Text Start', world.clearer_hints).text + '\x04'
							 | 
						||
| 
								 | 
							
								        for (reward, color) in bossRewardsSpiritualStones:
							 | 
						||
| 
								 | 
							
								            child_text += buildBossString(reward, color, world)
							 | 
						||
| 
								 | 
							
								    child_text += getHint('Child Altar Text End', world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								    child_text += '\x0B'
							 | 
						||
| 
								 | 
							
								    update_message_by_id(messages, 0x707A, get_raw_text(child_text), 0x20)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # text that appears at altar as an adult.
							 | 
						||
| 
								 | 
							
								    adult_text = '\x08'
							 | 
						||
| 
								 | 
							
								    adult_text += getHint('Adult Altar Text Start', world.clearer_hints).text + '\x04'
							 | 
						||
| 
								 | 
							
								    if include_rewards:
							 | 
						||
| 
								 | 
							
								        bossRewardsMedallions = [
							 | 
						||
| 
								 | 
							
								            ('Light Medallion',  'Light Blue'),
							 | 
						||
| 
								 | 
							
								            ('Forest Medallion', 'Green'),
							 | 
						||
| 
								 | 
							
								            ('Fire Medallion',   'Red'),
							 | 
						||
| 
								 | 
							
								            ('Water Medallion',  'Blue'),
							 | 
						||
| 
								 | 
							
								            ('Shadow Medallion', 'Pink'),
							 | 
						||
| 
								 | 
							
								            ('Spirit Medallion', 'Yellow'),
							 | 
						||
| 
								 | 
							
								        ]
							 | 
						||
| 
								 | 
							
								        for (reward, color) in bossRewardsMedallions:
							 | 
						||
| 
								 | 
							
								            adult_text += buildBossString(reward, color, world)
							 | 
						||
| 
								 | 
							
								    if include_wincons:
							 | 
						||
| 
								 | 
							
								        adult_text += buildBridgeReqsString(world)
							 | 
						||
| 
								 | 
							
								        adult_text += '\x04'
							 | 
						||
| 
								 | 
							
								        adult_text += buildGanonBossKeyString(world)
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        adult_text += getHint('Adult Altar Text End', world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								    adult_text += '\x0B'
							 | 
						||
| 
								 | 
							
								    update_message_by_id(messages, 0x7057, get_raw_text(adult_text), 0x20)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# pulls text string from hintlist for reward after sending the location to hintlist.
							 | 
						||
| 
								 | 
							
								def buildBossString(reward, color, world):
							 | 
						||
| 
								 | 
							
								    for location in world.world.get_filled_locations(world.player):
							 | 
						||
| 
								 | 
							
								        if location.item.name == reward:
							 | 
						||
| 
								 | 
							
								            item_icon = chr(location.item.special['item_id'])
							 | 
						||
| 
								 | 
							
								            location_text = getHint(location.name, world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								            return str(GossipText("\x08\x13%s%s" % (item_icon, location_text), [color], prefix='')) + '\x04'
							 | 
						||
| 
								 | 
							
								    return ''
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def buildBridgeReqsString(world):
							 | 
						||
| 
								 | 
							
								    string = "\x13\x12" # Light Arrow Icon
							 | 
						||
| 
								 | 
							
								    if world.bridge == 'open':
							 | 
						||
| 
								 | 
							
								        string += "The awakened ones will have #already created a bridge# to the castle where the evil dwells."
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        item_req_string = getHint('bridge_' + world.bridge, world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								        if world.bridge == 'medallions':
							 | 
						||
| 
								 | 
							
								            item_req_string = str(world.bridge_medallions) + ' ' + item_req_string
							 | 
						||
| 
								 | 
							
								        elif world.bridge == 'stones':
							 | 
						||
| 
								 | 
							
								            item_req_string = str(world.bridge_stones) + ' ' + item_req_string
							 | 
						||
| 
								 | 
							
								        elif world.bridge == 'dungeons':
							 | 
						||
| 
								 | 
							
								            item_req_string = str(world.bridge_rewards) + ' ' + item_req_string
							 | 
						||
| 
								 | 
							
								        elif world.bridge == 'tokens':
							 | 
						||
| 
								 | 
							
								            item_req_string = str(world.bridge_tokens) + ' ' + item_req_string
							 | 
						||
| 
								 | 
							
								        if '#' not in item_req_string:
							 | 
						||
| 
								 | 
							
								            item_req_string = '#%s#' % item_req_string
							 | 
						||
| 
								 | 
							
								        string += "The awakened ones will await for the Hero to collect %s." % item_req_string
							 | 
						||
| 
								 | 
							
								    return str(GossipText(string, ['Green'], prefix=''))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def buildGanonBossKeyString(world):
							 | 
						||
| 
								 | 
							
								    string = "\x13\x74" # Boss Key Icon
							 | 
						||
| 
								 | 
							
								    if world.shuffle_ganon_bosskey == 'remove':
							 | 
						||
| 
								 | 
							
								        string += "And the door to the \x05\x41evil one\x05\x40's chamber will be left #unlocked#."
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        if world.shuffle_ganon_bosskey == 'on_lacs':
							 | 
						||
| 
								 | 
							
								            item_req_string = getHint('lacs_' + world.lacs_condition, world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								            if world.lacs_condition == 'medallions':
							 | 
						||
| 
								 | 
							
								                item_req_string = str(world.lacs_medallions) + ' ' + item_req_string
							 | 
						||
| 
								 | 
							
								            elif world.lacs_condition == 'stones':
							 | 
						||
| 
								 | 
							
								                item_req_string = str(world.lacs_stones) + ' ' + item_req_string
							 | 
						||
| 
								 | 
							
								            elif world.lacs_condition == 'dungeons':
							 | 
						||
| 
								 | 
							
								                item_req_string = str(world.lacs_rewards) + ' ' + item_req_string
							 | 
						||
| 
								 | 
							
								            elif world.lacs_condition == 'tokens':
							 | 
						||
| 
								 | 
							
								                item_req_string = str(world.lacs_tokens) + ' ' + item_req_string
							 | 
						||
| 
								 | 
							
								            if '#' not in item_req_string:
							 | 
						||
| 
								 | 
							
								                item_req_string = '#%s#' % item_req_string
							 | 
						||
| 
								 | 
							
								            bk_location_string = "provided by Zelda once %s are retrieved" % item_req_string
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text
							 | 
						||
| 
								 | 
							
								        string += "And the \x05\x41evil one\x05\x40's key will be %s." % bk_location_string
							 | 
						||
| 
								 | 
							
								    return str(GossipText(string, ['Yellow'], prefix=''))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# fun new lines for Ganon during the final battle
							 | 
						||
| 
								 | 
							
								def buildGanonText(world, messages):
							 | 
						||
| 
								 | 
							
								    # empty now unused messages to make space for ganon lines
							 | 
						||
| 
								 | 
							
								    update_message_by_id(messages, 0x70C8, " ")
							 | 
						||
| 
								 | 
							
								    update_message_by_id(messages, 0x70C9, " ")
							 | 
						||
| 
								 | 
							
								    update_message_by_id(messages, 0x70CA, " ")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # lines before battle
							 | 
						||
| 
								 | 
							
								    ganonLines = getHintGroup('ganonLine', world)
							 | 
						||
| 
								 | 
							
								    world.hint_rng.shuffle(ganonLines)
							 | 
						||
| 
								 | 
							
								    text = get_raw_text(ganonLines.pop().text)
							 | 
						||
| 
								 | 
							
								    update_message_by_id(messages, 0x70CB, text)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # light arrow hint or validation chest item
							 | 
						||
| 
								 | 
							
								    if world.starting_items['Light Arrows'] > 0:
							 | 
						||
| 
								 | 
							
								        text = get_raw_text(getHint('Light Arrow Location', world.clearer_hints).text)
							 | 
						||
| 
								 | 
							
								        text += "\x05\x42your pocket\x05\x40"
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        try:
							 | 
						||
| 
								 | 
							
								            find_light_arrows = world.world.find_item('Light Arrows', world.player)
							 | 
						||
| 
								 | 
							
								            text = get_raw_text(getHint('Light Arrow Location', world.clearer_hints).text)
							 | 
						||
| 
								 | 
							
								            location = find_light_arrows
							 | 
						||
| 
								 | 
							
								            location_hint = get_hint_area(location)
							 | 
						||
| 
								 | 
							
								            if world.player != location.player:
							 | 
						||
| 
								 | 
							
								                text += "\x05\x42%s's\x05\x40 %s" % (world.world.get_player_name(location.player), get_raw_text(location_hint))
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                location_hint = location_hint.replace('Ganon\'s Castle', 'my castle')
							 | 
						||
| 
								 | 
							
								                text += get_raw_text(location_hint)
							 | 
						||
| 
								 | 
							
								        except StopIteration:
							 | 
						||
| 
								 | 
							
								            text = get_raw_text(getHint('Validation Line', world.clearer_hints).text)
							 | 
						||
| 
								 | 
							
								            for location in world.world.get_filled_locations(world.player):
							 | 
						||
| 
								 | 
							
								                if location.name == 'Ganons Tower Boss Key Chest':
							 | 
						||
| 
								 | 
							
								                    text += get_raw_text(getHint(getItemGenericName(location.item), world.clearer_hints).text)
							 | 
						||
| 
								 | 
							
								                    break
							 | 
						||
| 
								 | 
							
								    text += '!'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    update_message_by_id(messages, 0x70CC, text)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_raw_text(string):
							 | 
						||
| 
								 | 
							
								    text = ''
							 | 
						||
| 
								 | 
							
								    for char in string:
							 | 
						||
| 
								 | 
							
								        if char == '^':
							 | 
						||
| 
								 | 
							
								            text += '\x04' # box break
							 | 
						||
| 
								 | 
							
								        elif char == '&':
							 | 
						||
| 
								 | 
							
								            text += '\x01' # new line
							 | 
						||
| 
								 | 
							
								        elif char == '@':
							 | 
						||
| 
								 | 
							
								            text += '\x0F' # print player name
							 | 
						||
| 
								 | 
							
								        elif char == '#':
							 | 
						||
| 
								 | 
							
								            text += '\x05\x40' # sets color to white
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            text += char
							 | 
						||
| 
								 | 
							
								    return text
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def HintDistFiles():
							 | 
						||
| 
								 | 
							
								    return [os.path.join(data_path('Hints/'), d) for d in defaultHintDists] + [
							 | 
						||
| 
								 | 
							
								            os.path.join(data_path('Hints/'), d)
							 | 
						||
| 
								 | 
							
								            for d in sorted(os.listdir(data_path('Hints/')))
							 | 
						||
| 
								 | 
							
								            if d.endswith('.json') and d not in defaultHintDists]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def HintDistList():
							 | 
						||
| 
								 | 
							
								    dists = {}
							 | 
						||
| 
								 | 
							
								    for d in HintDistFiles():
							 | 
						||
| 
								 | 
							
								        dist = read_json(d)
							 | 
						||
| 
								 | 
							
								        dist_name = dist['name']
							 | 
						||
| 
								 | 
							
								        gui_name = dist['gui_name']
							 | 
						||
| 
								 | 
							
								        dists.update({ dist_name: gui_name })
							 | 
						||
| 
								 | 
							
								    return dists
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def HintDistTips():
							 | 
						||
| 
								 | 
							
								    tips = ""
							 | 
						||
| 
								 | 
							
								    first_dist = True
							 | 
						||
| 
								 | 
							
								    line_char_limit = 33
							 | 
						||
| 
								 | 
							
								    for d in HintDistFiles():
							 | 
						||
| 
								 | 
							
								        if not first_dist:
							 | 
						||
| 
								 | 
							
								            tips = tips + "\n"
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            first_dist = False
							 | 
						||
| 
								 | 
							
								        dist = read_json(d)
							 | 
						||
| 
								 | 
							
								        gui_name = dist['gui_name']
							 | 
						||
| 
								 | 
							
								        desc = dist['description']
							 | 
						||
| 
								 | 
							
								        i = 0
							 | 
						||
| 
								 | 
							
								        end_of_line = False
							 | 
						||
| 
								 | 
							
								        tips = tips + "<b>"
							 | 
						||
| 
								 | 
							
								        for c in gui_name:
							 | 
						||
| 
								 | 
							
								            if c == " " and end_of_line:
							 | 
						||
| 
								 | 
							
								                tips = tips + "\n"
							 | 
						||
| 
								 | 
							
								                end_of_line = False
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                tips = tips + c
							 | 
						||
| 
								 | 
							
								                i = i + 1
							 | 
						||
| 
								 | 
							
								                if i > line_char_limit:
							 | 
						||
| 
								 | 
							
								                    end_of_line = True
							 | 
						||
| 
								 | 
							
								                    i = 0
							 | 
						||
| 
								 | 
							
								        tips = tips + "</b>: "
							 | 
						||
| 
								 | 
							
								        i = i + 2
							 | 
						||
| 
								 | 
							
								        for c in desc:
							 | 
						||
| 
								 | 
							
								            if c == " " and end_of_line:
							 | 
						||
| 
								 | 
							
								                tips = tips + "\n"
							 | 
						||
| 
								 | 
							
								                end_of_line = False
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                tips = tips + c
							 | 
						||
| 
								 | 
							
								                i = i + 1
							 | 
						||
| 
								 | 
							
								                if i > line_char_limit:
							 | 
						||
| 
								 | 
							
								                    end_of_line = True
							 | 
						||
| 
								 | 
							
								                    i = 0
							 | 
						||
| 
								 | 
							
								        tips = tips + "\n"
							 | 
						||
| 
								 | 
							
								    return tips
							 |