694 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			694 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | from __future__ import annotations | ||
|  | 
 | ||
|  | import logging | ||
|  | import asyncio | ||
|  | import shutil | ||
|  | import time | ||
|  | 
 | ||
|  | import Utils | ||
|  | 
 | ||
|  | from NetUtils import ClientStatus, color | ||
|  | from worlds.AutoSNIClient import SNIClient | ||
|  | 
 | ||
|  | from worlds.alttp import Shops, Regions | ||
|  | from .Rom import ROM_PLAYER_LIMIT | ||
|  | 
 | ||
|  | snes_logger = logging.getLogger("SNES") | ||
|  | 
 | ||
|  | GAME_ALTTP = "A Link to the Past" | ||
|  | 
 | ||
|  | # FXPAK Pro protocol memory mapping used by SNI | ||
|  | ROM_START = 0x000000 | ||
|  | WRAM_START = 0xF50000 | ||
|  | WRAM_SIZE = 0x20000 | ||
|  | SRAM_START = 0xE00000 | ||
|  | 
 | ||
|  | ROMNAME_START = SRAM_START + 0x2000 | ||
|  | ROMNAME_SIZE = 0x15 | ||
|  | 
 | ||
|  | INGAME_MODES = {0x07, 0x09, 0x0b} | ||
|  | ENDGAME_MODES = {0x19, 0x1a} | ||
|  | DEATH_MODES = {0x12} | ||
|  | 
 | ||
|  | SAVEDATA_START = WRAM_START + 0xF000 | ||
|  | SAVEDATA_SIZE = 0x500 | ||
|  | 
 | ||
|  | RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0         # 2 bytes | ||
|  | RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2             # 1 byte | ||
|  | RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3      # 1 byte | ||
|  | ROOMID_ADDR = SAVEDATA_START + 0x4D4                # 2 bytes | ||
|  | ROOMDATA_ADDR = SAVEDATA_START + 0x4D6              # 1 byte | ||
|  | SCOUT_LOCATION_ADDR = SAVEDATA_START + 0x4D7        # 1 byte | ||
|  | SCOUTREPLY_LOCATION_ADDR = SAVEDATA_START + 0x4D8   # 1 byte | ||
|  | SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9       # 1 byte | ||
|  | SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA     # 1 byte | ||
|  | SHOP_ADDR = SAVEDATA_START + 0x302                  # 2 bytes | ||
|  | SHOP_LEN = (len(Shops.shop_table) * 3) + 5 | ||
|  | 
 | ||
|  | DEATH_LINK_ACTIVE_ADDR = ROMNAME_START + 0x15       # 1 byte | ||
|  | 
 | ||
|  | location_shop_ids = set([info[0] for name, info in Shops.shop_table.items()]) | ||
|  | 
 | ||
|  | location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10), | ||
|  |                      "Blind's Hideout - Left": (0x11d, 0x20), | ||
|  |                      "Blind's Hideout - Right": (0x11d, 0x40), | ||
|  |                      "Blind's Hideout - Far Left": (0x11d, 0x80), | ||
|  |                      "Blind's Hideout - Far Right": (0x11d, 0x100), | ||
|  |                      'Secret Passage': (0x55, 0x10), | ||
|  |                      'Waterfall Fairy - Left': (0x114, 0x10), | ||
|  |                      'Waterfall Fairy - Right': (0x114, 0x20), | ||
|  |                      "King's Tomb": (0x113, 0x10), | ||
|  |                      'Floodgate Chest': (0x10b, 0x10), | ||
|  |                      "Link's House": (0x104, 0x10), | ||
|  |                      'Kakariko Tavern': (0x103, 0x10), | ||
|  |                      'Chicken House': (0x108, 0x10), | ||
|  |                      "Aginah's Cave": (0x10a, 0x10), | ||
|  |                      "Sahasrahla's Hut - Left": (0x105, 0x10), | ||
|  |                      "Sahasrahla's Hut - Middle": (0x105, 0x20), | ||
|  |                      "Sahasrahla's Hut - Right": (0x105, 0x40), | ||
|  |                      'Kakariko Well - Top': (0x2f, 0x10), | ||
|  |                      'Kakariko Well - Left': (0x2f, 0x20), | ||
|  |                      'Kakariko Well - Middle': (0x2f, 0x40), | ||
|  |                      'Kakariko Well - Right': (0x2f, 0x80), | ||
|  |                      'Kakariko Well - Bottom': (0x2f, 0x100), | ||
|  |                      'Lost Woods Hideout': (0xe1, 0x200), | ||
|  |                      'Lumberjack Tree': (0xe2, 0x200), | ||
|  |                      'Cave 45': (0x11b, 0x400), | ||
|  |                      'Graveyard Cave': (0x11b, 0x200), | ||
|  |                      'Checkerboard Cave': (0x126, 0x200), | ||
|  |                      'Mini Moldorm Cave - Far Left': (0x123, 0x10), | ||
|  |                      'Mini Moldorm Cave - Left': (0x123, 0x20), | ||
|  |                      'Mini Moldorm Cave - Right': (0x123, 0x40), | ||
|  |                      'Mini Moldorm Cave - Far Right': (0x123, 0x80), | ||
|  |                      'Mini Moldorm Cave - Generous Guy': (0x123, 0x400), | ||
|  |                      'Ice Rod Cave': (0x120, 0x10), | ||
|  |                      'Bonk Rock Cave': (0x124, 0x10), | ||
|  |                      'Desert Palace - Big Chest': (0x73, 0x10), | ||
|  |                      'Desert Palace - Torch': (0x73, 0x400), | ||
|  |                      'Desert Palace - Map Chest': (0x74, 0x10), | ||
|  |                      'Desert Palace - Compass Chest': (0x85, 0x10), | ||
|  |                      'Desert Palace - Big Key Chest': (0x75, 0x10), | ||
|  |                      'Desert Palace - Desert Tiles 1 Pot Key': (0x63, 0x400), | ||
|  |                      'Desert Palace - Beamos Hall Pot Key': (0x53, 0x400), | ||
|  |                      'Desert Palace - Desert Tiles 2 Pot Key': (0x43, 0x400), | ||
|  |                      'Desert Palace - Boss': (0x33, 0x800), | ||
|  |                      'Eastern Palace - Compass Chest': (0xa8, 0x10), | ||
|  |                      'Eastern Palace - Big Chest': (0xa9, 0x10), | ||
|  |                      'Eastern Palace - Dark Square Pot Key': (0xba, 0x400), | ||
|  |                      'Eastern Palace - Dark Eyegore Key Drop': (0x99, 0x400), | ||
|  |                      'Eastern Palace - Cannonball Chest': (0xb9, 0x10), | ||
|  |                      'Eastern Palace - Big Key Chest': (0xb8, 0x10), | ||
|  |                      'Eastern Palace - Map Chest': (0xaa, 0x10), | ||
|  |                      'Eastern Palace - Boss': (0xc8, 0x800), | ||
|  |                      'Hyrule Castle - Boomerang Chest': (0x71, 0x10), | ||
|  |                      'Hyrule Castle - Boomerang Guard Key Drop': (0x71, 0x400), | ||
|  |                      'Hyrule Castle - Map Chest': (0x72, 0x10), | ||
|  |                      'Hyrule Castle - Map Guard Key Drop': (0x72, 0x400), | ||
|  |                      "Hyrule Castle - Zelda's Chest": (0x80, 0x10), | ||
|  |                      'Hyrule Castle - Big Key Drop': (0x80, 0x400), | ||
|  |                      'Sewers - Dark Cross': (0x32, 0x10), | ||
|  |                      'Hyrule Castle - Key Rat Key Drop': (0x21, 0x400), | ||
|  |                      'Sewers - Secret Room - Left': (0x11, 0x10), | ||
|  |                      'Sewers - Secret Room - Middle': (0x11, 0x20), | ||
|  |                      'Sewers - Secret Room - Right': (0x11, 0x40), | ||
|  |                      'Sanctuary': (0x12, 0x10), | ||
|  |                      'Castle Tower - Room 03': (0xe0, 0x10), | ||
|  |                      'Castle Tower - Dark Maze': (0xd0, 0x10), | ||
|  |                      'Castle Tower - Dark Archer Key Drop': (0xc0, 0x400), | ||
|  |                      'Castle Tower - Circle of Pots Key Drop': (0xb0, 0x400), | ||
|  |                      'Spectacle Rock Cave': (0xea, 0x400), | ||
|  |                      'Paradox Cave Lower - Far Left': (0xef, 0x10), | ||
|  |                      'Paradox Cave Lower - Left': (0xef, 0x20), | ||
|  |                      'Paradox Cave Lower - Right': (0xef, 0x40), | ||
|  |                      'Paradox Cave Lower - Far Right': (0xef, 0x80), | ||
|  |                      'Paradox Cave Lower - Middle': (0xef, 0x100), | ||
|  |                      'Paradox Cave Upper - Left': (0xff, 0x10), | ||
|  |                      'Paradox Cave Upper - Right': (0xff, 0x20), | ||
|  |                      'Spiral Cave': (0xfe, 0x10), | ||
|  |                      'Tower of Hera - Basement Cage': (0x87, 0x400), | ||
|  |                      'Tower of Hera - Map Chest': (0x77, 0x10), | ||
|  |                      'Tower of Hera - Big Key Chest': (0x87, 0x10), | ||
|  |                      'Tower of Hera - Compass Chest': (0x27, 0x20), | ||
|  |                      'Tower of Hera - Big Chest': (0x27, 0x10), | ||
|  |                      'Tower of Hera - Boss': (0x7, 0x800), | ||
|  |                      'Hype Cave - Top': (0x11e, 0x10), | ||
|  |                      'Hype Cave - Middle Right': (0x11e, 0x20), | ||
|  |                      'Hype Cave - Middle Left': (0x11e, 0x40), | ||
|  |                      'Hype Cave - Bottom': (0x11e, 0x80), | ||
|  |                      'Hype Cave - Generous Guy': (0x11e, 0x400), | ||
|  |                      'Peg Cave': (0x127, 0x400), | ||
|  |                      'Pyramid Fairy - Left': (0x116, 0x10), | ||
|  |                      'Pyramid Fairy - Right': (0x116, 0x20), | ||
|  |                      'Brewery': (0x106, 0x10), | ||
|  |                      'C-Shaped House': (0x11c, 0x10), | ||
|  |                      'Chest Game': (0x106, 0x400), | ||
|  |                      'Mire Shed - Left': (0x10d, 0x10), | ||
|  |                      'Mire Shed - Right': (0x10d, 0x20), | ||
|  |                      'Superbunny Cave - Top': (0xf8, 0x10), | ||
|  |                      'Superbunny Cave - Bottom': (0xf8, 0x20), | ||
|  |                      'Spike Cave': (0x117, 0x10), | ||
|  |                      'Hookshot Cave - Top Right': (0x3c, 0x10), | ||
|  |                      'Hookshot Cave - Top Left': (0x3c, 0x20), | ||
|  |                      'Hookshot Cave - Bottom Right': (0x3c, 0x80), | ||
|  |                      'Hookshot Cave - Bottom Left': (0x3c, 0x40), | ||
|  |                      'Mimic Cave': (0x10c, 0x10), | ||
|  |                      'Swamp Palace - Entrance': (0x28, 0x10), | ||
|  |                      'Swamp Palace - Map Chest': (0x37, 0x10), | ||
|  |                      'Swamp Palace - Pot Row Pot Key': (0x38, 0x400), | ||
|  |                      'Swamp Palace - Trench 1 Pot Key': (0x37, 0x400), | ||
|  |                      'Swamp Palace - Hookshot Pot Key': (0x36, 0x400), | ||
|  |                      'Swamp Palace - Big Chest': (0x36, 0x10), | ||
|  |                      'Swamp Palace - Compass Chest': (0x46, 0x10), | ||
|  |                      'Swamp Palace - Trench 2 Pot Key': (0x35, 0x400), | ||
|  |                      'Swamp Palace - Big Key Chest': (0x35, 0x10), | ||
|  |                      'Swamp Palace - West Chest': (0x34, 0x10), | ||
|  |                      'Swamp Palace - Flooded Room - Left': (0x76, 0x10), | ||
|  |                      'Swamp Palace - Flooded Room - Right': (0x76, 0x20), | ||
|  |                      'Swamp Palace - Waterfall Room': (0x66, 0x10), | ||
|  |                      'Swamp Palace - Waterway Pot Key': (0x16, 0x400), | ||
|  |                      'Swamp Palace - Boss': (0x6, 0x800), | ||
|  |                      "Thieves' Town - Big Key Chest": (0xdb, 0x20), | ||
|  |                      "Thieves' Town - Map Chest": (0xdb, 0x10), | ||
|  |                      "Thieves' Town - Compass Chest": (0xdc, 0x10), | ||
|  |                      "Thieves' Town - Ambush Chest": (0xcb, 0x10), | ||
|  |                      "Thieves' Town - Hallway Pot Key": (0xbc, 0x400), | ||
|  |                      "Thieves' Town - Spike Switch Pot Key": (0xab, 0x400), | ||
|  |                      "Thieves' Town - Attic": (0x65, 0x10), | ||
|  |                      "Thieves' Town - Big Chest": (0x44, 0x10), | ||
|  |                      "Thieves' Town - Blind's Cell": (0x45, 0x10), | ||
|  |                      "Thieves' Town - Boss": (0xac, 0x800), | ||
|  |                      'Skull Woods - Compass Chest': (0x67, 0x10), | ||
|  |                      'Skull Woods - Map Chest': (0x58, 0x20), | ||
|  |                      'Skull Woods - Big Chest': (0x58, 0x10), | ||
|  |                      'Skull Woods - Pot Prison': (0x57, 0x20), | ||
|  |                      'Skull Woods - Pinball Room': (0x68, 0x10), | ||
|  |                      'Skull Woods - Big Key Chest': (0x57, 0x10), | ||
|  |                      'Skull Woods - West Lobby Pot Key': (0x56, 0x400), | ||
|  |                      'Skull Woods - Bridge Room': (0x59, 0x10), | ||
|  |                      'Skull Woods - Spike Corner Key Drop': (0x39, 0x400), | ||
|  |                      'Skull Woods - Boss': (0x29, 0x800), | ||
|  |                      'Ice Palace - Jelly Key Drop': (0x0e, 0x400), | ||
|  |                      'Ice Palace - Compass Chest': (0x2e, 0x10), | ||
|  |                      'Ice Palace - Conveyor Key Drop': (0x3e, 0x400), | ||
|  |                      'Ice Palace - Freezor Chest': (0x7e, 0x10), | ||
|  |                      'Ice Palace - Big Chest': (0x9e, 0x10), | ||
|  |                      'Ice Palace - Iced T Room': (0xae, 0x10), | ||
|  |                      'Ice Palace - Many Pots Pot Key': (0x9f, 0x400), | ||
|  |                      'Ice Palace - Spike Room': (0x5f, 0x10), | ||
|  |                      'Ice Palace - Big Key Chest': (0x1f, 0x10), | ||
|  |                      'Ice Palace - Hammer Block Key Drop': (0x3f, 0x400), | ||
|  |                      'Ice Palace - Map Chest': (0x3f, 0x10), | ||
|  |                      'Ice Palace - Boss': (0xde, 0x800), | ||
|  |                      'Misery Mire - Big Chest': (0xc3, 0x10), | ||
|  |                      'Misery Mire - Map Chest': (0xc3, 0x20), | ||
|  |                      'Misery Mire - Main Lobby': (0xc2, 0x10), | ||
|  |                      'Misery Mire - Bridge Chest': (0xa2, 0x10), | ||
|  |                      'Misery Mire - Spikes Pot Key': (0xb3, 0x400), | ||
|  |                      'Misery Mire - Spike Chest': (0xb3, 0x10), | ||
|  |                      'Misery Mire - Fishbone Pot Key': (0xa1, 0x400), | ||
|  |                      'Misery Mire - Conveyor Crystal Key Drop': (0xc1, 0x400), | ||
|  |                      'Misery Mire - Compass Chest': (0xc1, 0x10), | ||
|  |                      'Misery Mire - Big Key Chest': (0xd1, 0x10), | ||
|  |                      'Misery Mire - Boss': (0x90, 0x800), | ||
|  |                      'Turtle Rock - Compass Chest': (0xd6, 0x10), | ||
|  |                      'Turtle Rock - Roller Room - Left': (0xb7, 0x10), | ||
|  |                      'Turtle Rock - Roller Room - Right': (0xb7, 0x20), | ||
|  |                      'Turtle Rock - Pokey 1 Key Drop': (0xb6, 0x400), | ||
|  |                      'Turtle Rock - Chain Chomps': (0xb6, 0x10), | ||
|  |                      'Turtle Rock - Pokey 2 Key Drop': (0x13, 0x400), | ||
|  |                      'Turtle Rock - Big Key Chest': (0x14, 0x10), | ||
|  |                      'Turtle Rock - Big Chest': (0x24, 0x10), | ||
|  |                      'Turtle Rock - Crystaroller Room': (0x4, 0x10), | ||
|  |                      'Turtle Rock - Eye Bridge - Bottom Left': (0xd5, 0x80), | ||
|  |                      'Turtle Rock - Eye Bridge - Bottom Right': (0xd5, 0x40), | ||
|  |                      'Turtle Rock - Eye Bridge - Top Left': (0xd5, 0x20), | ||
|  |                      'Turtle Rock - Eye Bridge - Top Right': (0xd5, 0x10), | ||
|  |                      'Turtle Rock - Boss': (0xa4, 0x800), | ||
|  |                      'Palace of Darkness - Shooter Room': (0x9, 0x10), | ||
|  |                      'Palace of Darkness - The Arena - Bridge': (0x2a, 0x20), | ||
|  |                      'Palace of Darkness - Stalfos Basement': (0xa, 0x10), | ||
|  |                      'Palace of Darkness - Big Key Chest': (0x3a, 0x10), | ||
|  |                      'Palace of Darkness - The Arena - Ledge': (0x2a, 0x10), | ||
|  |                      'Palace of Darkness - Map Chest': (0x2b, 0x10), | ||
|  |                      'Palace of Darkness - Compass Chest': (0x1a, 0x20), | ||
|  |                      'Palace of Darkness - Dark Basement - Left': (0x6a, 0x10), | ||
|  |                      'Palace of Darkness - Dark Basement - Right': (0x6a, 0x20), | ||
|  |                      'Palace of Darkness - Dark Maze - Top': (0x19, 0x10), | ||
|  |                      'Palace of Darkness - Dark Maze - Bottom': (0x19, 0x20), | ||
|  |                      'Palace of Darkness - Big Chest': (0x1a, 0x10), | ||
|  |                      'Palace of Darkness - Harmless Hellway': (0x1a, 0x40), | ||
|  |                      'Palace of Darkness - Boss': (0x5a, 0x800), | ||
|  |                      'Ganons Tower - Conveyor Cross Pot Key': (0x8b, 0x400), | ||
|  |                      "Ganons Tower - Bob's Torch": (0x8c, 0x400), | ||
|  |                      'Ganons Tower - Hope Room - Left': (0x8c, 0x20), | ||
|  |                      'Ganons Tower - Hope Room - Right': (0x8c, 0x40), | ||
|  |                      'Ganons Tower - Tile Room': (0x8d, 0x10), | ||
|  |                      'Ganons Tower - Compass Room - Top Left': (0x9d, 0x10), | ||
|  |                      'Ganons Tower - Compass Room - Top Right': (0x9d, 0x20), | ||
|  |                      'Ganons Tower - Compass Room - Bottom Left': (0x9d, 0x40), | ||
|  |                      'Ganons Tower - Compass Room - Bottom Right': (0x9d, 0x80), | ||
|  |                      'Ganons Tower - Conveyor Star Pits Pot Key': (0x7b, 0x400), | ||
|  |                      'Ganons Tower - DMs Room - Top Left': (0x7b, 0x10), | ||
|  |                      'Ganons Tower - DMs Room - Top Right': (0x7b, 0x20), | ||
|  |                      'Ganons Tower - DMs Room - Bottom Left': (0x7b, 0x40), | ||
|  |                      'Ganons Tower - DMs Room - Bottom Right': (0x7b, 0x80), | ||
|  |                      'Ganons Tower - Map Chest': (0x8b, 0x10), | ||
|  |                      'Ganons Tower - Double Switch Pot Key': (0x9b, 0x400), | ||
|  |                      'Ganons Tower - Firesnake Room': (0x7d, 0x10), | ||
|  |                      'Ganons Tower - Randomizer Room - Top Left': (0x7c, 0x10), | ||
|  |                      'Ganons Tower - Randomizer Room - Top Right': (0x7c, 0x20), | ||
|  |                      'Ganons Tower - Randomizer Room - Bottom Left': (0x7c, 0x40), | ||
|  |                      'Ganons Tower - Randomizer Room - Bottom Right': (0x7c, 0x80), | ||
|  |                      "Ganons Tower - Bob's Chest": (0x8c, 0x80), | ||
|  |                      'Ganons Tower - Big Chest': (0x8c, 0x10), | ||
|  |                      'Ganons Tower - Big Key Room - Left': (0x1c, 0x20), | ||
|  |                      'Ganons Tower - Big Key Room - Right': (0x1c, 0x40), | ||
|  |                      'Ganons Tower - Big Key Chest': (0x1c, 0x10), | ||
|  |                      'Ganons Tower - Mini Helmasaur Room - Left': (0x3d, 0x10), | ||
|  |                      'Ganons Tower - Mini Helmasaur Room - Right': (0x3d, 0x20), | ||
|  |                      'Ganons Tower - Mini Helmasaur Key Drop': (0x3d, 0x400), | ||
|  |                      'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40), | ||
|  |                      'Ganons Tower - Validation Chest': (0x4d, 0x10)} | ||
|  | 
 | ||
|  | boss_locations = {Regions.lookup_name_to_id[name] for name in {'Eastern Palace - Boss', | ||
|  |                                                                'Desert Palace - Boss', | ||
|  |                                                                'Tower of Hera - Boss', | ||
|  |                                                                'Palace of Darkness - Boss', | ||
|  |                                                                'Swamp Palace - Boss', | ||
|  |                                                                'Skull Woods - Boss', | ||
|  |                                                                "Thieves' Town - Boss", | ||
|  |                                                                'Ice Palace - Boss', | ||
|  |                                                                'Misery Mire - Boss', | ||
|  |                                                                'Turtle Rock - Boss', | ||
|  |                                                                'Sahasrahla'}} | ||
|  | 
 | ||
|  | location_table_uw_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_uw.items()} | ||
|  | 
 | ||
|  | location_table_npc = {'Mushroom': 0x1000, | ||
|  |                       'King Zora': 0x2, | ||
|  |                       'Sahasrahla': 0x10, | ||
|  |                       'Blacksmith': 0x400, | ||
|  |                       'Magic Bat': 0x8000, | ||
|  |                       'Sick Kid': 0x4, | ||
|  |                       'Library': 0x80, | ||
|  |                       'Potion Shop': 0x2000, | ||
|  |                       'Old Man': 0x1, | ||
|  |                       'Ether Tablet': 0x100, | ||
|  |                       'Catfish': 0x20, | ||
|  |                       'Stumpy': 0x8, | ||
|  |                       'Bombos Tablet': 0x200} | ||
|  | 
 | ||
|  | location_table_npc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_npc.items()} | ||
|  | 
 | ||
|  | location_table_ow = {'Flute Spot': 0x2a, | ||
|  |                      'Sunken Treasure': 0x3b, | ||
|  |                      "Zora's Ledge": 0x81, | ||
|  |                      'Lake Hylia Island': 0x35, | ||
|  |                      'Maze Race': 0x28, | ||
|  |                      'Desert Ledge': 0x30, | ||
|  |                      'Master Sword Pedestal': 0x80, | ||
|  |                      'Spectacle Rock': 0x3, | ||
|  |                      'Pyramid': 0x5b, | ||
|  |                      'Digging Game': 0x68, | ||
|  |                      'Bumper Cave Ledge': 0x4a, | ||
|  |                      'Floating Island': 0x5} | ||
|  | 
 | ||
|  | location_table_ow_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_ow.items()} | ||
|  | 
 | ||
|  | location_table_misc = {'Bottle Merchant': (0x3c9, 0x2), | ||
|  |                        'Purple Chest': (0x3c9, 0x10), | ||
|  |                        "Link's Uncle": (0x3c6, 0x1), | ||
|  |                        'Hobo': (0x3c9, 0x1)} | ||
|  | location_table_misc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_misc.items()} | ||
|  | 
 | ||
|  | 
 | ||
|  | async def track_locations(ctx, roomid, roomdata): | ||
|  |     from SNIClient import snes_read, snes_buffered_write, snes_flush_writes | ||
|  |     new_locations = [] | ||
|  | 
 | ||
|  |     def new_check(location_id): | ||
|  |         new_locations.append(location_id) | ||
|  |         ctx.locations_checked.add(location_id) | ||
|  |         location = ctx.location_names[location_id] | ||
|  |         snes_logger.info( | ||
|  |             f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') | ||
|  | 
 | ||
|  |     try: | ||
|  |         shop_data = await snes_read(ctx, SHOP_ADDR, SHOP_LEN) | ||
|  |         shop_data_changed = False | ||
|  |         shop_data = list(shop_data) | ||
|  |         for cnt, b in enumerate(shop_data): | ||
|  |             location = Shops.SHOP_ID_START + cnt | ||
|  |             if int(b) and location not in ctx.locations_checked: | ||
|  |                 new_check(location) | ||
|  |             if ctx.allow_collect and location in ctx.checked_locations and location not in ctx.locations_checked \ | ||
|  |                     and location in ctx.locations_info and ctx.locations_info[location].player != ctx.slot: | ||
|  |                 if not int(b): | ||
|  |                     shop_data[cnt] += 1 | ||
|  |                     shop_data_changed = True | ||
|  |         if shop_data_changed: | ||
|  |             snes_buffered_write(ctx, SHOP_ADDR, bytes(shop_data)) | ||
|  |     except Exception as e: | ||
|  |         snes_logger.info(f"Exception: {e}") | ||
|  | 
 | ||
|  |     for location_id, (loc_roomid, loc_mask) in location_table_uw_id.items(): | ||
|  |         try: | ||
|  |             if location_id not in ctx.locations_checked and loc_roomid == roomid and \ | ||
|  |                     (roomdata << 4) & loc_mask != 0: | ||
|  |                 new_check(location_id) | ||
|  |         except Exception as e: | ||
|  |             snes_logger.exception(f"Exception: {e}") | ||
|  | 
 | ||
|  |     uw_begin = 0x129 | ||
|  |     ow_end = uw_end = 0 | ||
|  |     uw_unchecked = {} | ||
|  |     uw_checked = {} | ||
|  |     for location, (roomid, mask) in location_table_uw.items(): | ||
|  |         location_id = Regions.lookup_name_to_id[location] | ||
|  |         if location_id not in ctx.locations_checked: | ||
|  |             uw_unchecked[location_id] = (roomid, mask) | ||
|  |             uw_begin = min(uw_begin, roomid) | ||
|  |             uw_end = max(uw_end, roomid + 1) | ||
|  |         if ctx.allow_collect and location_id not in boss_locations and location_id in ctx.checked_locations \ | ||
|  |                 and location_id not in ctx.locations_checked and location_id in ctx.locations_info \ | ||
|  |                 and ctx.locations_info[location_id].player != ctx.slot: | ||
|  |             uw_begin = min(uw_begin, roomid) | ||
|  |             uw_end = max(uw_end, roomid + 1) | ||
|  |             uw_checked[location_id] = (roomid, mask) | ||
|  | 
 | ||
|  |     if uw_begin < uw_end: | ||
|  |         uw_data = await snes_read(ctx, SAVEDATA_START + (uw_begin * 2), (uw_end - uw_begin) * 2) | ||
|  |         if uw_data is not None: | ||
|  |             for location_id, (roomid, mask) in uw_unchecked.items(): | ||
|  |                 offset = (roomid - uw_begin) * 2 | ||
|  |                 roomdata = uw_data[offset] | (uw_data[offset + 1] << 8) | ||
|  |                 if roomdata & mask != 0: | ||
|  |                     new_check(location_id) | ||
|  |             if uw_checked: | ||
|  |                 uw_data = list(uw_data) | ||
|  |                 for location_id, (roomid, mask) in uw_checked.items(): | ||
|  |                     offset = (roomid - uw_begin) * 2 | ||
|  |                     roomdata = uw_data[offset] | (uw_data[offset + 1] << 8) | ||
|  |                     roomdata |= mask | ||
|  |                     uw_data[offset] = roomdata & 0xFF | ||
|  |                     uw_data[offset + 1] = roomdata >> 8 | ||
|  |                 snes_buffered_write(ctx, SAVEDATA_START + (uw_begin * 2), bytes(uw_data)) | ||
|  | 
 | ||
|  |     ow_begin = 0x82 | ||
|  |     ow_unchecked = {} | ||
|  |     ow_checked = {} | ||
|  |     for location_id, screenid in location_table_ow_id.items(): | ||
|  |         if location_id not in ctx.locations_checked: | ||
|  |             ow_unchecked[location_id] = screenid | ||
|  |             ow_begin = min(ow_begin, screenid) | ||
|  |             ow_end = max(ow_end, screenid + 1) | ||
|  |             if ctx.allow_collect and location_id in ctx.checked_locations and location_id in ctx.locations_info \ | ||
|  |                     and ctx.locations_info[location_id].player != ctx.slot: | ||
|  |                 ow_checked[location_id] = screenid | ||
|  | 
 | ||
|  |     if ow_begin < ow_end: | ||
|  |         ow_data = await snes_read(ctx, SAVEDATA_START + 0x280 + ow_begin, ow_end - ow_begin) | ||
|  |         if ow_data is not None: | ||
|  |             for location_id, screenid in ow_unchecked.items(): | ||
|  |                 if ow_data[screenid - ow_begin] & 0x40 != 0: | ||
|  |                     new_check(location_id) | ||
|  |             if ow_checked: | ||
|  |                 ow_data = list(ow_data) | ||
|  |                 for location_id, screenid in ow_checked.items(): | ||
|  |                     ow_data[screenid - ow_begin] |= 0x40 | ||
|  |                 snes_buffered_write(ctx, SAVEDATA_START + 0x280 + ow_begin, bytes(ow_data)) | ||
|  | 
 | ||
|  |     if not ctx.locations_checked.issuperset(location_table_npc_id): | ||
|  |         npc_data = await snes_read(ctx, SAVEDATA_START + 0x410, 2) | ||
|  |         if npc_data is not None: | ||
|  |             npc_value_changed = False | ||
|  |             npc_value = npc_data[0] | (npc_data[1] << 8) | ||
|  |             for location_id, mask in location_table_npc_id.items(): | ||
|  |                 if npc_value & mask != 0 and location_id not in ctx.locations_checked: | ||
|  |                     new_check(location_id) | ||
|  |                 if ctx.allow_collect and location_id not in boss_locations and location_id in ctx.checked_locations \ | ||
|  |                         and location_id not in ctx.locations_checked and location_id in ctx.locations_info \ | ||
|  |                         and ctx.locations_info[location_id].player != ctx.slot: | ||
|  |                     npc_value |= mask | ||
|  |                     npc_value_changed = True | ||
|  |             if npc_value_changed: | ||
|  |                 npc_data = bytes([npc_value & 0xFF, npc_value >> 8]) | ||
|  |                 snes_buffered_write(ctx, SAVEDATA_START + 0x410, npc_data) | ||
|  | 
 | ||
|  |     if not ctx.locations_checked.issuperset(location_table_misc_id): | ||
|  |         misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4) | ||
|  |         if misc_data is not None: | ||
|  |             misc_data = list(misc_data) | ||
|  |             misc_data_changed = False | ||
|  |             for location_id, (offset, mask) in location_table_misc_id.items(): | ||
|  |                 assert (0x3c6 <= offset <= 0x3c9) | ||
|  |                 if misc_data[offset - 0x3c6] & mask != 0 and location_id not in ctx.locations_checked: | ||
|  |                     new_check(location_id) | ||
|  |                 if ctx.allow_collect and location_id in ctx.checked_locations and location_id not in ctx.locations_checked \ | ||
|  |                         and location_id in ctx.locations_info and ctx.locations_info[location_id].player != ctx.slot: | ||
|  |                     misc_data_changed = True | ||
|  |                     misc_data[offset - 0x3c6] |= mask | ||
|  |             if misc_data_changed: | ||
|  |                 snes_buffered_write(ctx, SAVEDATA_START + 0x3c6, bytes(misc_data)) | ||
|  | 
 | ||
|  | 
 | ||
|  |     if new_locations: | ||
|  |         await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}]) | ||
|  |     await snes_flush_writes(ctx) | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_alttp_settings(romfile: str): | ||
|  |     lastSettings = Utils.get_adjuster_settings(GAME_ALTTP) | ||
|  |     adjustedromfile = '' | ||
|  |     if lastSettings: | ||
|  |         choice = 'no' | ||
|  |         if not hasattr(lastSettings, 'auto_apply') or 'ask' in lastSettings.auto_apply: | ||
|  | 
 | ||
|  |             whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap", | ||
|  |                          "uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes", | ||
|  |                          "reduceflashing", "deathlink", "allowcollect"} | ||
|  |             printed_options = {name: value for name, value in vars(lastSettings).items() if name in whitelist} | ||
|  |             if hasattr(lastSettings, "sprite_pool"): | ||
|  |                 sprite_pool = {} | ||
|  |                 for sprite in lastSettings.sprite_pool: | ||
|  |                     if sprite in sprite_pool: | ||
|  |                         sprite_pool[sprite] += 1 | ||
|  |                     else: | ||
|  |                         sprite_pool[sprite] = 1 | ||
|  |                     if sprite_pool: | ||
|  |                         printed_options["sprite_pool"] = sprite_pool | ||
|  |             import pprint | ||
|  | 
 | ||
|  |             from CommonClient import gui_enabled | ||
|  |             if gui_enabled: | ||
|  | 
 | ||
|  |                 try: | ||
|  |                     from tkinter import Tk, PhotoImage, Label, LabelFrame, Frame, Button | ||
|  |                     applyPromptWindow = Tk() | ||
|  |                 except Exception as e: | ||
|  |                     logging.error('Could not load tkinter, which is likely not installed.') | ||
|  |                     return '', False | ||
|  | 
 | ||
|  |                 applyPromptWindow.resizable(False, False) | ||
|  |                 applyPromptWindow.protocol('WM_DELETE_WINDOW', lambda: onButtonClick()) | ||
|  |                 logo = PhotoImage(file=Utils.local_path('data', 'icon.png')) | ||
|  |                 applyPromptWindow.tk.call('wm', 'iconphoto', applyPromptWindow._w, logo) | ||
|  |                 applyPromptWindow.wm_title("Last adjuster settings LttP") | ||
|  | 
 | ||
|  |                 label = LabelFrame(applyPromptWindow, | ||
|  |                                    text='Last used adjuster settings were found. Would you like to apply these?') | ||
|  |                 label.grid(column=0, row=0, padx=5, pady=5, ipadx=5, ipady=5) | ||
|  |                 label.grid_columnconfigure(0, weight=1) | ||
|  |                 label.grid_columnconfigure(1, weight=1) | ||
|  |                 label.grid_columnconfigure(2, weight=1) | ||
|  |                 label.grid_columnconfigure(3, weight=1) | ||
|  | 
 | ||
|  |                 def onButtonClick(answer: str = 'no'): | ||
|  |                     setattr(onButtonClick, 'choice', answer) | ||
|  |                     applyPromptWindow.destroy() | ||
|  | 
 | ||
|  |                 framedOptions = Frame(label) | ||
|  |                 framedOptions.grid(column=0, columnspan=4, row=0) | ||
|  |                 framedOptions.grid_columnconfigure(0, weight=1) | ||
|  |                 framedOptions.grid_columnconfigure(1, weight=1) | ||
|  |                 framedOptions.grid_columnconfigure(2, weight=1) | ||
|  |                 curRow = 0 | ||
|  |                 curCol = 0 | ||
|  |                 for name, value in printed_options.items(): | ||
|  |                     Label(framedOptions, text=name + ": " + str(value)).grid(column=curCol, row=curRow, padx=5) | ||
|  |                     if (curCol == 2): | ||
|  |                         curRow += 1 | ||
|  |                         curCol = 0 | ||
|  |                     else: | ||
|  |                         curCol += 1 | ||
|  | 
 | ||
|  |                 yesButton = Button(label, text='Yes', command=lambda: onButtonClick('yes'), width=10) | ||
|  |                 yesButton.grid(column=0, row=1) | ||
|  |                 noButton = Button(label, text='No', command=lambda: onButtonClick('no'), width=10) | ||
|  |                 noButton.grid(column=1, row=1) | ||
|  |                 alwaysButton = Button(label, text='Always', command=lambda: onButtonClick('always'), width=10) | ||
|  |                 alwaysButton.grid(column=2, row=1) | ||
|  |                 neverButton = Button(label, text='Never', command=lambda: onButtonClick('never'), width=10) | ||
|  |                 neverButton.grid(column=3, row=1) | ||
|  | 
 | ||
|  |                 Utils.tkinter_center_window(applyPromptWindow) | ||
|  |                 applyPromptWindow.mainloop() | ||
|  |                 choice = getattr(onButtonClick, 'choice') | ||
|  |             else: | ||
|  |                 choice = input(f"Last used adjuster settings were found. Would you like to apply these? \n" | ||
|  |                                f"{pprint.pformat(printed_options)}\n" | ||
|  |                                f"Enter yes, no, always or never: ") | ||
|  |             if choice and choice.startswith("y"): | ||
|  |                 choice = 'yes' | ||
|  |             elif choice and "never" in choice: | ||
|  |                 choice = 'no' | ||
|  |                 lastSettings.auto_apply = 'never' | ||
|  |                 Utils.persistent_store("adjuster", GAME_ALTTP, lastSettings) | ||
|  |             elif choice and "always" in choice: | ||
|  |                 choice = 'yes' | ||
|  |                 lastSettings.auto_apply = 'always' | ||
|  |                 Utils.persistent_store("adjuster", GAME_ALTTP, lastSettings) | ||
|  |             else: | ||
|  |                 choice = 'no' | ||
|  |         elif 'never' in lastSettings.auto_apply: | ||
|  |             choice = 'no' | ||
|  |         elif 'always' in lastSettings.auto_apply: | ||
|  |             choice = 'yes' | ||
|  | 
 | ||
|  |         if 'yes' in choice: | ||
|  |             from worlds.alttp.Rom import get_base_rom_path | ||
|  |             lastSettings.rom = romfile | ||
|  |             lastSettings.baserom = get_base_rom_path() | ||
|  |             lastSettings.world = None | ||
|  | 
 | ||
|  |             if hasattr(lastSettings, "sprite_pool"): | ||
|  |                 from LttPAdjuster import AdjusterWorld | ||
|  |                 lastSettings.world = AdjusterWorld(getattr(lastSettings, "sprite_pool")) | ||
|  | 
 | ||
|  |             adjusted = True | ||
|  |             import LttPAdjuster | ||
|  |             _, adjustedromfile = LttPAdjuster.adjust(lastSettings) | ||
|  | 
 | ||
|  |             if hasattr(lastSettings, "world"): | ||
|  |                 delattr(lastSettings, "world") | ||
|  |         else: | ||
|  |             adjusted = False | ||
|  |         if adjusted: | ||
|  |             try: | ||
|  |                 shutil.move(adjustedromfile, romfile) | ||
|  |                 adjustedromfile = romfile | ||
|  |             except Exception as e: | ||
|  |                 logging.exception(e) | ||
|  |     else: | ||
|  |         adjusted = False | ||
|  |     return adjustedromfile, adjusted | ||
|  | 
 | ||
|  | 
 | ||
|  | class ALTTPSNIClient(SNIClient): | ||
|  |     game = "A Link to the Past" | ||
|  | 
 | ||
|  |     async def deathlink_kill_player(self, ctx): | ||
|  |         from SNIClient import DeathState, snes_read, snes_buffered_write, snes_flush_writes | ||
|  |         invincible = await snes_read(ctx, WRAM_START + 0x037B, 1) | ||
|  |         last_health = await snes_read(ctx, WRAM_START + 0xF36D, 1) | ||
|  |         await asyncio.sleep(0.25) | ||
|  |         health = await snes_read(ctx, WRAM_START + 0xF36D, 1) | ||
|  |         if not invincible or not last_health or not health: | ||
|  |             ctx.death_state = DeathState.dead | ||
|  |             ctx.last_death_link = time.time() | ||
|  |             return | ||
|  |         if not invincible[0] and last_health[0] == health[0]: | ||
|  |             snes_buffered_write(ctx, WRAM_START + 0xF36D, bytes([0]))  # set current health to 0 | ||
|  |             snes_buffered_write(ctx, WRAM_START + 0x0373, | ||
|  |                                 bytes([8]))  # deal 1 full heart of damage at next opportunity | ||
|  | 
 | ||
|  |         await snes_flush_writes(ctx) | ||
|  |         await asyncio.sleep(1) | ||
|  | 
 | ||
|  |         gamemode = await snes_read(ctx, WRAM_START + 0x10, 1) | ||
|  |         if not gamemode or gamemode[0] in DEATH_MODES: | ||
|  |             ctx.death_state = DeathState.dead | ||
|  | 
 | ||
|  | 
 | ||
|  |     async def validate_rom(self, ctx): | ||
|  |         from SNIClient import snes_read, snes_buffered_write, snes_flush_writes | ||
|  | 
 | ||
|  |         rom_name = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE) | ||
|  |         if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"AP": | ||
|  |             return False | ||
|  | 
 | ||
|  |         ctx.game = self.game | ||
|  |         ctx.items_handling = 0b001  # full local | ||
|  | 
 | ||
|  |         ctx.rom = rom_name | ||
|  | 
 | ||
|  |         death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1) | ||
|  | 
 | ||
|  |         if death_link: | ||
|  |             ctx.allow_collect = bool(death_link[0] & 0b100) | ||
|  |             ctx.death_link_allow_survive = bool(death_link[0] & 0b10) | ||
|  |             await ctx.update_death_link(bool(death_link[0] & 0b1)) | ||
|  | 
 | ||
|  |         return True | ||
|  | 
 | ||
|  | 
 | ||
|  |     async def game_watcher(self, ctx): | ||
|  |         from SNIClient import snes_read, snes_buffered_write, snes_flush_writes | ||
|  |         gamemode = await snes_read(ctx, WRAM_START + 0x10, 1) | ||
|  |         if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time(): | ||
|  |             currently_dead = gamemode[0] in DEATH_MODES | ||
|  |             await ctx.handle_deathlink_state(currently_dead) | ||
|  | 
 | ||
|  |         gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1) | ||
|  |         game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4) | ||
|  |         if gamemode is None or gameend is None or game_timer is None or \ | ||
|  |                 (gamemode[0] not in INGAME_MODES and gamemode[0] not in ENDGAME_MODES): | ||
|  |             return | ||
|  | 
 | ||
|  |         if gameend[0]: | ||
|  |             if not ctx.finished_game: | ||
|  |                 await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) | ||
|  |                 ctx.finished_game = True | ||
|  | 
 | ||
|  |         if gamemode in ENDGAME_MODES:  # triforce room and credits | ||
|  |             return | ||
|  | 
 | ||
|  |         data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8) | ||
|  |         if data is None: | ||
|  |             return | ||
|  | 
 | ||
|  |         recv_index = data[0] | (data[1] << 8) | ||
|  |         recv_item = data[2] | ||
|  |         roomid = data[4] | (data[5] << 8) | ||
|  |         roomdata = data[6] | ||
|  |         scout_location = data[7] | ||
|  | 
 | ||
|  |         if recv_index < len(ctx.items_received) and recv_item == 0: | ||
|  |             item = ctx.items_received[recv_index] | ||
|  |             recv_index += 1 | ||
|  |             logging.info('Received %s from %s (%s) (%d/%d in list)' % ( | ||
|  |                 color(ctx.item_names[item.item], 'red', 'bold'), | ||
|  |                 color(ctx.player_names[item.player], 'yellow'), | ||
|  |                 ctx.location_names[item.location], recv_index, len(ctx.items_received))) | ||
|  | 
 | ||
|  |             snes_buffered_write(ctx, RECV_PROGRESS_ADDR, | ||
|  |                                 bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) | ||
|  |             snes_buffered_write(ctx, RECV_ITEM_ADDR, | ||
|  |                                 bytes([item.item])) | ||
|  |             snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, | ||
|  |                                 bytes([min(ROM_PLAYER_LIMIT, item.player) if item.player != ctx.slot else 0])) | ||
|  |         if scout_location > 0 and scout_location in ctx.locations_info: | ||
|  |             snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR, | ||
|  |                                 bytes([scout_location])) | ||
|  |             snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR, | ||
|  |                                 bytes([ctx.locations_info[scout_location].item])) | ||
|  |             snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR, | ||
|  |                                 bytes([min(ROM_PLAYER_LIMIT, ctx.locations_info[scout_location].player)])) | ||
|  | 
 | ||
|  |         await snes_flush_writes(ctx) | ||
|  | 
 | ||
|  |         if scout_location > 0 and scout_location not in ctx.locations_scouted: | ||
|  |             ctx.locations_scouted.add(scout_location) | ||
|  |             await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}]) | ||
|  |         await track_locations(ctx, roomid, roomdata) |