multiworld

This commit is contained in:
Bonta-kun 2019-12-09 19:27:56 +01:00
parent ce19713209
commit 55a30aa91f
15 changed files with 2070 additions and 678 deletions

View File

@ -38,6 +38,7 @@ def main():
Alternatively, can be a ALttP Rom patched with a Link Alternatively, can be a ALttP Rom patched with a Link
sprite that will be extracted. sprite that will be extracted.
''') ''')
parser.add_argument('--names', default='', type=str)
args = parser.parse_args() args = parser.parse_args()
# ToDo: Validate files further than mere existance # ToDo: Validate files further than mere existance

View File

@ -1,8 +1,9 @@
import os import os
import re
import time import time
import logging import logging
from Utils import output_path from Utils import output_path, parse_names_string
from Rom import LocalRom, Sprite, apply_rom_settings from Rom import LocalRom, Sprite, apply_rom_settings
@ -26,7 +27,7 @@ def adjust(args):
else: else:
raise RuntimeError('Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.') raise RuntimeError('Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, sprite) apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, sprite, parse_names_string(args.names))
rom.write_to_file(output_path('%s.sfc' % outfilebase)) rom.write_to_file(output_path('%s.sfc' % outfilebase))

View File

@ -781,12 +781,13 @@ class Boss(object):
return self.defeat_rule(state, self.player) return self.defeat_rule(state, self.player)
class Location(object): class Location(object):
def __init__(self, player, name='', address=None, crystal=False, hint_text=None, parent=None): def __init__(self, player, name='', address=None, crystal=False, hint_text=None, parent=None, player_address=None):
self.name = name self.name = name
self.parent_region = parent self.parent_region = parent
self.item = None self.item = None
self.crystal = crystal self.crystal = crystal
self.address = address self.address = address
self.player_address = player_address
self.spot_type = 'Location' self.spot_type = 'Location'
self.hint_text = hint_text if hint_text is not None else 'Hyrule' self.hint_text = hint_text if hint_text is not None else 'Hyrule'
self.recursion_count = 0 self.recursion_count = 0
@ -1033,6 +1034,7 @@ class Spoiler(object):
'accessibility': self.world.accessibility, 'accessibility': self.world.accessibility,
'hints': self.world.hints, 'hints': self.world.hints,
'keysanity': self.world.keysanity, 'keysanity': self.world.keysanity,
'players': self.world.players
} }
def to_json(self): def to_json(self):

View File

@ -255,7 +255,7 @@ def start():
parser.add_argument('--shufflepalette', default=False, action='store_true') parser.add_argument('--shufflepalette', default=False, action='store_true')
parser.add_argument('--shufflepots', default=False, action='store_true') parser.add_argument('--shufflepots', default=False, action='store_true')
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255)) parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--names', default='')
parser.add_argument('--outputpath') parser.add_argument('--outputpath')
args = parser.parse_args() args = parser.parse_args()

29
Gui.py
View File

@ -13,7 +13,7 @@ from AdjusterMain import adjust
from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress
from Main import main, __version__ as ESVersion from Main import main, __version__ as ESVersion
from Rom import Sprite from Rom import Sprite
from Utils import is_bundled, local_path, output_path, open_file from Utils import is_bundled, local_path, output_path, open_file, parse_names_string
def guiMain(args=None): def guiMain(args=None):
@ -346,6 +346,9 @@ def guiMain(args=None):
worldLabel = Label(bottomFrame, text='Worlds') worldLabel = Label(bottomFrame, text='Worlds')
worldVar = StringVar() worldVar = StringVar()
worldSpinbox = Spinbox(bottomFrame, from_=1, to=100, width=5, textvariable=worldVar) worldSpinbox = Spinbox(bottomFrame, from_=1, to=100, width=5, textvariable=worldVar)
namesLabel = Label(bottomFrame, text='Player names')
namesVar = StringVar()
namesEntry = Entry(bottomFrame, textvariable=namesVar)
seedLabel = Label(bottomFrame, text='Seed #') seedLabel = Label(bottomFrame, text='Seed #')
seedVar = StringVar() seedVar = StringVar()
seedEntry = Entry(bottomFrame, width=15, textvariable=seedVar) seedEntry = Entry(bottomFrame, width=15, textvariable=seedVar)
@ -356,6 +359,7 @@ def guiMain(args=None):
def generateRom(): def generateRom():
guiargs = Namespace guiargs = Namespace
guiargs.multi = int(worldVar.get()) guiargs.multi = int(worldVar.get())
guiargs.names = namesVar.get()
guiargs.seed = int(seedVar.get()) if seedVar.get() else None guiargs.seed = int(seedVar.get()) if seedVar.get() else None
guiargs.count = int(countVar.get()) if countVar.get() != '1' else None guiargs.count = int(countVar.get()) if countVar.get() != '1' else None
guiargs.mode = modeVar.get() guiargs.mode = modeVar.get()
@ -416,12 +420,18 @@ def guiMain(args=None):
except Exception as e: except Exception as e:
messagebox.showerror(title="Error while creating seed", message=str(e)) messagebox.showerror(title="Error while creating seed", message=str(e))
else: else:
messagebox.showinfo(title="Success", message="Rom patched successfully") msgtxt = "Rom patched successfully"
if guiargs.names:
for player, name in parse_names_string(guiargs.names).items():
msgtxt += "\nPlayer %d => %s" % (player, name)
messagebox.showinfo(title="Success", message=msgtxt)
generateButton = Button(bottomFrame, text='Generate Patched Rom', command=generateRom) generateButton = Button(bottomFrame, text='Generate Patched Rom', command=generateRom)
worldLabel.pack(side=LEFT) worldLabel.pack(side=LEFT)
worldSpinbox.pack(side=LEFT) worldSpinbox.pack(side=LEFT)
namesLabel.pack(side=LEFT)
namesEntry.pack(side=LEFT)
seedLabel.pack(side=LEFT, padx=(5, 0)) seedLabel.pack(side=LEFT, padx=(5, 0))
seedEntry.pack(side=LEFT) seedEntry.pack(side=LEFT)
countLabel.pack(side=LEFT, padx=(5, 0)) countLabel.pack(side=LEFT, padx=(5, 0))
@ -502,10 +512,18 @@ def guiMain(args=None):
fastMenuLabel2 = Label(fastMenuFrame2, text='Menu speed') fastMenuLabel2 = Label(fastMenuFrame2, text='Menu speed')
fastMenuLabel2.pack(side=LEFT) fastMenuLabel2.pack(side=LEFT)
namesFrame2 = Frame(drowDownFrame2)
namesLabel2 = Label(namesFrame2, text='Player names')
namesVar2 = StringVar()
namesEntry2 = Entry(namesFrame2, textvariable=namesVar2)
namesLabel2.pack(side=LEFT)
namesEntry2.pack(side=LEFT)
heartbeepFrame2.pack(expand=True, anchor=E) heartbeepFrame2.pack(expand=True, anchor=E)
heartcolorFrame2.pack(expand=True, anchor=E) heartcolorFrame2.pack(expand=True, anchor=E)
fastMenuFrame2.pack(expand=True, anchor=E) fastMenuFrame2.pack(expand=True, anchor=E)
namesFrame2.pack(expand=True, anchor=E)
bottomFrame2 = Frame(topFrame2) bottomFrame2 = Frame(topFrame2)
@ -518,12 +536,17 @@ def guiMain(args=None):
guiargs.disablemusic = bool(disableMusicVar.get()) guiargs.disablemusic = bool(disableMusicVar.get())
guiargs.rom = romVar2.get() guiargs.rom = romVar2.get()
guiargs.sprite = sprite guiargs.sprite = sprite
guiargs.names = namesEntry2.get()
try: try:
adjust(args=guiargs) adjust(args=guiargs)
except Exception as e: except Exception as e:
messagebox.showerror(title="Error while creating seed", message=str(e)) messagebox.showerror(title="Error while creating seed", message=str(e))
else: else:
messagebox.showinfo(title="Success", message="Rom patched successfully") msgtxt = "Rom patched successfully"
if guiargs.names:
for player, name in parse_names_string(guiargs.names).items():
msgtxt += "\nPlayer %d => %s" % (player, name)
messagebox.showinfo(title="Success", message=msgtxt)
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom) adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)

View File

@ -340,8 +340,8 @@ def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None
for exit in exits: for exit in exits:
ret.exits.append(Entrance(player, exit, ret)) ret.exits.append(Entrance(player, exit, ret))
for location in locations: for location in locations:
address, crystal, hint_text = location_table[location] address, player_address, crystal, hint_text = location_table[location]
ret.locations.append(Location(player, location, address, crystal, hint_text, ret)) ret.locations.append(Location(player, location, address, crystal, hint_text, ret, player_address))
return ret return ret
def mark_dark_world_regions(world): def mark_dark_world_regions(world):
@ -406,236 +406,236 @@ default_shop_contents = {
'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)], 'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)],
} }
location_table = {'Mushroom': (0x180013, False, 'in the woods'), location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
'Bottle Merchant': (0x2EB18, False, 'with a merchant'), 'Bottle Merchant': (0x2eb18, 0x186339, False, 'with a merchant'),
'Flute Spot': (0x18014A, False, 'underground'), 'Flute Spot': (0x18014a, 0x18633d, False, 'underground'),
'Sunken Treasure': (0x180145, False, 'underwater'), 'Sunken Treasure': (0x180145, 0x186354, False, 'underwater'),
'Purple Chest': (0x33D68, False, 'from a box'), 'Purple Chest': (0x33d68, 0x186359, False, 'from a box'),
'Blind\'s Hideout - Top': (0xEB0F, False, 'in a basement'), "Blind's Hideout - Top": (0xeb0f, 0x1862e3, False, 'in a basement'),
'Blind\'s Hideout - Left': (0xEB12, False, 'in a basement'), "Blind's Hideout - Left": (0xeb12, 0x1862e6, False, 'in a basement'),
'Blind\'s Hideout - Right': (0xEB15, False, 'in a basement'), "Blind's Hideout - Right": (0xeb15, 0x1862e9, False, 'in a basement'),
'Blind\'s Hideout - Far Left': (0xEB18, False, 'in a basement'), "Blind's Hideout - Far Left": (0xeb18, 0x1862ec, False, 'in a basement'),
'Blind\'s Hideout - Far Right': (0xEB1B, False, 'in a basement'), "Blind's Hideout - Far Right": (0xeb1b, 0x1862ef, False, 'in a basement'),
'Link\'s Uncle': (0x2DF45, False, 'with your uncle'), "Link's Uncle": (0x2df45, 0x18635f, False, 'with your uncle'),
'Secret Passage': (0xE971, False, 'near your uncle'), 'Secret Passage': (0xe971, 0x186145, False, 'near your uncle'),
'King Zora': (0xEE1C3, False, 'at a high price'), 'King Zora': (0xee1c3, 0x186360, False, 'at a high price'),
'Zora\'s Ledge': (0x180149, False, 'near Zora'), "Zora's Ledge": (0x180149, 0x186358, False, 'near Zora'),
'Waterfall Fairy - Left': (0xE9B0, False, 'near a fairy'), 'Waterfall Fairy - Left': (0xe9b0, 0x186184, False, 'near a fairy'),
'Waterfall Fairy - Right': (0xE9D1, False, 'near a fairy'), 'Waterfall Fairy - Right': (0xe9d1, 0x1861a5, False, 'near a fairy'),
'King\'s Tomb': (0xE97A, False, 'alone in a cave'), "King's Tomb": (0xe97a, 0x18614e, False, 'alone in a cave'),
'Floodgate Chest': (0xE98C, False, 'in the dam'), 'Floodgate Chest': (0xe98c, 0x186160, False, 'in the dam'),
'Link\'s House': (0xE9BC, False, 'in your home'), "Link's House": (0xe9bc, 0x186190, False, 'in your home'),
'Kakariko Tavern': (0xE9CE, False, 'in the bar'), 'Kakariko Tavern': (0xe9ce, 0x1861a2, False, 'in the bar'),
'Chicken House': (0xE9E9, False, 'near poultry'), 'Chicken House': (0xe9e9, 0x1861bd, False, 'near poultry'),
'Aginah\'s Cave': (0xE9F2, False, 'with Aginah'), "Aginah's Cave": (0xe9f2, 0x1861c6, False, 'with Aginah'),
'Sahasrahla\'s Hut - Left': (0xEA82, False, 'near the elder'), "Sahasrahla's Hut - Left": (0xea82, 0x186256, False, 'near the elder'),
'Sahasrahla\'s Hut - Middle': (0xEA85, False, 'near the elder'), "Sahasrahla's Hut - Middle": (0xea85, 0x186259, False, 'near the elder'),
'Sahasrahla\'s Hut - Right': (0xEA88, False, 'near the elder'), "Sahasrahla's Hut - Right": (0xea88, 0x18625c, False, 'near the elder'),
'Sahasrahla': (0x2F1FC, False, 'with the elder'), 'Sahasrahla': (0x2f1fc, 0x186365, False, 'with the elder'),
'Kakariko Well - Top': (0xEA8E, False, 'in a well'), 'Kakariko Well - Top': (0xea8e, 0x186262, False, 'in a well'),
'Kakariko Well - Left': (0xEA91, False, 'in a well'), 'Kakariko Well - Left': (0xea91, 0x186265, False, 'in a well'),
'Kakariko Well - Middle': (0xEA94, False, 'in a well'), 'Kakariko Well - Middle': (0xea94, 0x186268, False, 'in a well'),
'Kakariko Well - Right': (0xEA97, False, 'in a well'), 'Kakariko Well - Right': (0xea97, 0x18626b, False, 'in a well'),
'Kakariko Well - Bottom': (0xEA9A, False, 'in a well'), 'Kakariko Well - Bottom': (0xea9a, 0x18626e, False, 'in a well'),
'Blacksmith': (0x18002A, False, 'with the smith'), 'Blacksmith': (0x18002a, 0x186366, False, 'with the smith'),
'Magic Bat': (0x180015, False, 'with the bat'), 'Magic Bat': (0x180015, 0x18635e, False, 'with the bat'),
'Sick Kid': (0x339CF, False, 'with the sick'), 'Sick Kid': (0x339cf, 0x186367, False, 'with the sick'),
'Hobo': (0x33E7D, False, 'with the hobo'), 'Hobo': (0x33e7d, 0x186368, False, 'with the hobo'),
'Lost Woods Hideout': (0x180000, False, 'near a thief'), 'Lost Woods Hideout': (0x180000, 0x186348, False, 'near a thief'),
'Lumberjack Tree': (0x180001, False, 'in a hole'), 'Lumberjack Tree': (0x180001, 0x186349, False, 'in a hole'),
'Cave 45': (0x180003, False, 'alone in a cave'), 'Cave 45': (0x180003, 0x18634b, False, 'alone in a cave'),
'Graveyard Cave': (0x180004, False, 'alone in a cave'), 'Graveyard Cave': (0x180004, 0x18634c, False, 'alone in a cave'),
'Checkerboard Cave': (0x180005, False, 'alone in a cave'), 'Checkerboard Cave': (0x180005, 0x18634d, False, 'alone in a cave'),
'Mini Moldorm Cave - Far Left': (0xEB42, False, 'near Moldorms'), 'Mini Moldorm Cave - Far Left': (0xeb42, 0x186316, False, 'near Moldorms'),
'Mini Moldorm Cave - Left': (0xEB45, False, 'near Moldorms'), 'Mini Moldorm Cave - Left': (0xeb45, 0x186319, False, 'near Moldorms'),
'Mini Moldorm Cave - Right': (0xEB48, False, 'near Moldorms'), 'Mini Moldorm Cave - Right': (0xeb48, 0x18631c, False, 'near Moldorms'),
'Mini Moldorm Cave - Far Right': (0xEB4B, False, 'near Moldorms'), 'Mini Moldorm Cave - Far Right': (0xeb4b, 0x18631f, False, 'near Moldorms'),
'Mini Moldorm Cave - Generous Guy': (0x180010, False, 'near Moldorms'), 'Mini Moldorm Cave - Generous Guy': (0x180010, 0x18635a, False, 'near Moldorms'),
'Ice Rod Cave': (0xEB4E, False, 'in a frozen cave'), 'Ice Rod Cave': (0xeb4e, 0x186322, False, 'in a frozen cave'),
'Bonk Rock Cave': (0xEB3F, False, 'alone in a cave'), 'Bonk Rock Cave': (0xeb3f, 0x186313, False, 'alone in a cave'),
'Library': (0x180012, False, 'near books'), 'Library': (0x180012, 0x18635c, False, 'near books'),
'Potion Shop': (0x180014, False, 'near potions'), 'Potion Shop': (0x180014, 0x18635d, False, 'near potions'),
'Lake Hylia Island': (0x180144, False, 'on an island'), 'Lake Hylia Island': (0x180144, 0x186353, False, 'on an island'),
'Maze Race': (0x180142, False, 'at the race'), 'Maze Race': (0x180142, 0x186351, False, 'at the race'),
'Desert Ledge': (0x180143, False, 'in the desert'), 'Desert Ledge': (0x180143, 0x186352, False, 'in the desert'),
'Desert Palace - Big Chest': (0xE98F, False, 'in Desert Palace'), 'Desert Palace - Big Chest': (0xe98f, 0x186163, False, 'in Desert Palace'),
'Desert Palace - Torch': (0x180160, False, 'in Desert Palace'), 'Desert Palace - Torch': (0x180160, 0x186362, False, 'in Desert Palace'),
'Desert Palace - Map Chest': (0xE9B6, False, 'in Desert Palace'), 'Desert Palace - Map Chest': (0xe9b6, 0x18618a, False, 'in Desert Palace'),
'Desert Palace - Compass Chest': (0xE9CB, False, 'in Desert Palace'), 'Desert Palace - Compass Chest': (0xe9cb, 0x18619f, False, 'in Desert Palace'),
'Desert Palace - Big Key Chest': (0xE9C2, False, 'in Desert Palace'), 'Desert Palace - Big Key Chest': (0xe9c2, 0x186196, False, 'in Desert Palace'),
'Desert Palace - Boss': (0x180151, False, 'with Lanmolas'), 'Desert Palace - Boss': (0x180151, 0x18633f, False, 'with Lanmolas'),
'Eastern Palace - Compass Chest': (0xE977, False, 'in Eastern Palace'), 'Eastern Palace - Compass Chest': (0xe977, 0x18614b, False, 'in Eastern Palace'),
'Eastern Palace - Big Chest': (0xE97D, False, 'in Eastern Palace'), 'Eastern Palace - Big Chest': (0xe97d, 0x186151, False, 'in Eastern Palace'),
'Eastern Palace - Cannonball Chest': (0xE9B3, False, 'in Eastern Palace'), 'Eastern Palace - Cannonball Chest': (0xe9b3, 0x186187, False, 'in Eastern Palace'),
'Eastern Palace - Big Key Chest': (0xE9B9, False, 'in Eastern Palace'), 'Eastern Palace - Big Key Chest': (0xe9b9, 0x18618d, False, 'in Eastern Palace'),
'Eastern Palace - Map Chest': (0xE9F5, False, 'in Eastern Palace'), 'Eastern Palace - Map Chest': (0xe9f5, 0x1861c9, False, 'in Eastern Palace'),
'Eastern Palace - Boss': (0x180150, False, 'with the Armos'), 'Eastern Palace - Boss': (0x180150, 0x18633e, False, 'with the Armos'),
'Master Sword Pedestal': (0x289B0, False, 'at the pedestal'), 'Master Sword Pedestal': (0x289b0, 0x186369, False, 'at the pedestal'),
'Hyrule Castle - Boomerang Chest': (0xE974, False, 'in Hyrule Castle'), 'Hyrule Castle - Boomerang Chest': (0xe974, 0x186148, False, 'in Hyrule Castle'),
'Hyrule Castle - Map Chest': (0xEB0C, False, 'in Hyrule Castle'), 'Hyrule Castle - Map Chest': (0xeb0c, 0x1862e0, False, 'in Hyrule Castle'),
'Hyrule Castle - Zelda\'s Chest': (0xEB09, False, 'in Hyrule Castle'), "Hyrule Castle - Zelda's Chest": (0xeb09, 0x1862dd, False, 'in Hyrule Castle'),
'Sewers - Dark Cross': (0xE96E, False, 'in the sewers'), 'Sewers - Dark Cross': (0xe96e, 0x186142, False, 'in the sewers'),
'Sewers - Secret Room - Left': (0xEB5D, False, 'in the sewers'), 'Sewers - Secret Room - Left': (0xeb5d, 0x186331, False, 'in the sewers'),
'Sewers - Secret Room - Middle': (0xEB60, False, 'in the sewers'), 'Sewers - Secret Room - Middle': (0xeb60, 0x186334, False, 'in the sewers'),
'Sewers - Secret Room - Right': (0xEB63, False, 'in the sewers'), 'Sewers - Secret Room - Right': (0xeb63, 0x186337, False, 'in the sewers'),
'Sanctuary': (0xEA79, False, 'in Sanctuary'), 'Sanctuary': (0xea79, 0x18624d, False, 'in Sanctuary'),
'Castle Tower - Room 03': (0xEAB5, False, 'in Castle Tower'), 'Castle Tower - Room 03': (0xeab5, 0x186289, False, 'in Castle Tower'),
'Castle Tower - Dark Maze': (0xEAB2, False, 'in Castle Tower'), 'Castle Tower - Dark Maze': (0xeab2, 0x186286, False, 'in Castle Tower'),
'Old Man': (0xF69FA, False, 'with the old man'), 'Old Man': (0xf69fa, 0x186364, False, 'with the old man'),
'Spectacle Rock Cave': (0x180002, False, 'alone in a cave'), 'Spectacle Rock Cave': (0x180002, 0x18634a, False, 'alone in a cave'),
'Paradox Cave Lower - Far Left': (0xEB2A, False, 'in a cave with seven chests'), 'Paradox Cave Lower - Far Left': (0xeb2a, 0x1862fe, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Left': (0xEB2D, False, 'in a cave with seven chests'), 'Paradox Cave Lower - Left': (0xeb2d, 0x186301, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Right': (0xEB30, False, 'in a cave with seven chests'), 'Paradox Cave Lower - Right': (0xeb30, 0x186304, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Far Right': (0xEB33, False, 'in a cave with seven chests'), 'Paradox Cave Lower - Far Right': (0xeb33, 0x186307, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Middle': (0xEB36, False, 'in a cave with seven chests'), 'Paradox Cave Lower - Middle': (0xeb36, 0x18630a, False, 'in a cave with seven chests'),
'Paradox Cave Upper - Left': (0xEB39, False, 'in a cave with seven chests'), 'Paradox Cave Upper - Left': (0xeb39, 0x18630d, False, 'in a cave with seven chests'),
'Paradox Cave Upper - Right': (0xEB3C, False, 'in a cave with seven chests'), 'Paradox Cave Upper - Right': (0xeb3c, 0x186310, False, 'in a cave with seven chests'),
'Spiral Cave': (0xE9BF, False, 'in spiral cave'), 'Spiral Cave': (0xe9bf, 0x186193, False, 'in spiral cave'),
'Ether Tablet': (0x180016, False, 'at a monolith'), 'Ether Tablet': (0x180016, 0x18633b, False, 'at a monolith'),
'Spectacle Rock': (0x180140, False, 'atop a rock'), 'Spectacle Rock': (0x180140, 0x18634f, False, 'atop a rock'),
'Tower of Hera - Basement Cage': (0x180162, False, 'in Tower of Hera'), 'Tower of Hera - Basement Cage': (0x180162, 0x18633a, False, 'in Tower of Hera'),
'Tower of Hera - Map Chest': (0xE9AD, False, 'in Tower of Hera'), 'Tower of Hera - Map Chest': (0xe9ad, 0x186181, False, 'in Tower of Hera'),
'Tower of Hera - Big Key Chest': (0xE9E6, False, 'in Tower of Hera'), 'Tower of Hera - Big Key Chest': (0xe9e6, 0x1861ba, False, 'in Tower of Hera'),
'Tower of Hera - Compass Chest': (0xE9FB, False, 'in Tower of Hera'), 'Tower of Hera - Compass Chest': (0xe9fb, 0x1861cf, False, 'in Tower of Hera'),
'Tower of Hera - Big Chest': (0xE9F8, False, 'in Tower of Hera'), 'Tower of Hera - Big Chest': (0xe9f8, 0x1861cc, False, 'in Tower of Hera'),
'Tower of Hera - Boss': (0x180152, False, 'with Moldorm'), 'Tower of Hera - Boss': (0x180152, 0x186340, False, 'with Moldorm'),
'Pyramid': (0x180147, False, 'on the pyramid'), 'Pyramid': (0x180147, 0x186356, False, 'on the pyramid'),
'Catfish': (0xEE185, False, 'with a catfish'), 'Catfish': (0xee185, 0x186361, False, 'with a catfish'),
'Stumpy': (0x330C7, False, 'with tree boy'), 'Stumpy': (0x330c7, 0x18636a, False, 'with tree boy'),
'Digging Game': (0x180148, False, 'underground'), 'Digging Game': (0x180148, 0x186357, False, 'underground'),
'Bombos Tablet': (0x180017, False, 'at a monolith'), 'Bombos Tablet': (0x180017, 0x18633c, False, 'at a monolith'),
'Hype Cave - Top': (0xEB1E, False, 'near a bat-like man'), 'Hype Cave - Top': (0xeb1e, 0x1862f2, False, 'near a bat-like man'),
'Hype Cave - Middle Right': (0xEB21, False, 'near a bat-like man'), 'Hype Cave - Middle Right': (0xeb21, 0x1862f5, False, 'near a bat-like man'),
'Hype Cave - Middle Left': (0xEB24, False, 'near a bat-like man'), 'Hype Cave - Middle Left': (0xeb24, 0x1862f8, False, 'near a bat-like man'),
'Hype Cave - Bottom': (0xEB27, False, 'near a bat-like man'), 'Hype Cave - Bottom': (0xeb27, 0x1862fb, False, 'near a bat-like man'),
'Hype Cave - Generous Guy': (0x180011, False, 'with a bat-like man'), 'Hype Cave - Generous Guy': (0x180011, 0x18635b, False, 'with a bat-like man'),
'Peg Cave': (0x180006, False, 'alone in a cave'), 'Peg Cave': (0x180006, 0x18634e, False, 'alone in a cave'),
'Pyramid Fairy - Left': (0xE980, False, 'near a fairy'), 'Pyramid Fairy - Left': (0xe980, 0x186154, False, 'near a fairy'),
'Pyramid Fairy - Right': (0xE983, False, 'near a fairy'), 'Pyramid Fairy - Right': (0xe983, 0x186157, False, 'near a fairy'),
'Brewery': (0xE9EC, False, 'alone in a home'), 'Brewery': (0xe9ec, 0x1861c0, False, 'alone in a home'),
'C-Shaped House': (0xE9EF, False, 'alone in a home'), 'C-Shaped House': (0xe9ef, 0x1861c3, False, 'alone in a home'),
'Chest Game': (0xEDA8, False, 'as a prize'), 'Chest Game': (0xeda8, 0x18636b, False, 'as a prize'),
'Bumper Cave Ledge': (0x180146, False, 'on a ledge'), 'Bumper Cave Ledge': (0x180146, 0x186355, False, 'on a ledge'),
'Mire Shed - Left': (0xEA73, False, 'near sparks'), 'Mire Shed - Left': (0xea73, 0x186247, False, 'near sparks'),
'Mire Shed - Right': (0xEA76, False, 'near sparks'), 'Mire Shed - Right': (0xea76, 0x18624a, False, 'near sparks'),
'Superbunny Cave - Top': (0xEA7C, False, 'in a connection'), 'Superbunny Cave - Top': (0xea7c, 0x186250, False, 'in a connection'),
'Superbunny Cave - Bottom': (0xEA7F, False, 'in a connection'), 'Superbunny Cave - Bottom': (0xea7f, 0x186253, False, 'in a connection'),
'Spike Cave': (0xEA8B, False, 'beyond spikes'), 'Spike Cave': (0xea8b, 0x18625f, False, 'beyond spikes'),
'Hookshot Cave - Top Right': (0xEB51, False, 'across pits'), 'Hookshot Cave - Top Right': (0xeb51, 0x186325, False, 'across pits'),
'Hookshot Cave - Top Left': (0xEB54, False, 'across pits'), 'Hookshot Cave - Top Left': (0xeb54, 0x186328, False, 'across pits'),
'Hookshot Cave - Bottom Right': (0xEB5A, False, 'across pits'), 'Hookshot Cave - Bottom Right': (0xeb5a, 0x18632e, False, 'across pits'),
'Hookshot Cave - Bottom Left': (0xEB57, False, 'across pits'), 'Hookshot Cave - Bottom Left': (0xeb57, 0x18632b, False, 'across pits'),
'Floating Island': (0x180141, False, 'on an island'), 'Floating Island': (0x180141, 0x186350, False, 'on an island'),
'Mimic Cave': (0xE9C5, False, 'in a cave of mimicry'), 'Mimic Cave': (0xe9c5, 0x186199, False, 'in a cave of mimicry'),
'Swamp Palace - Entrance': (0xEA9D, False, 'in Swamp Palace'), 'Swamp Palace - Entrance': (0xea9d, 0x186271, False, 'in Swamp Palace'),
'Swamp Palace - Map Chest': (0xE986, False, 'in Swamp Palace'), 'Swamp Palace - Map Chest': (0xe986, 0x18615a, False, 'in Swamp Palace'),
'Swamp Palace - Big Chest': (0xE989, False, 'in Swamp Palace'), 'Swamp Palace - Big Chest': (0xe989, 0x18615d, False, 'in Swamp Palace'),
'Swamp Palace - Compass Chest': (0xEAA0, False, 'in Swamp Palace'), 'Swamp Palace - Compass Chest': (0xeaa0, 0x186274, False, 'in Swamp Palace'),
'Swamp Palace - Big Key Chest': (0xEAA6, False, 'in Swamp Palace'), 'Swamp Palace - Big Key Chest': (0xeaa6, 0x18627a, False, 'in Swamp Palace'),
'Swamp Palace - West Chest': (0xEAA3, False, 'in Swamp Palace'), 'Swamp Palace - West Chest': (0xeaa3, 0x186277, False, 'in Swamp Palace'),
'Swamp Palace - Flooded Room - Left': (0xEAA9, False, 'in Swamp Palace'), 'Swamp Palace - Flooded Room - Left': (0xeaa9, 0x18627d, False, 'in Swamp Palace'),
'Swamp Palace - Flooded Room - Right': (0xEAAC, False, 'in Swamp Palace'), 'Swamp Palace - Flooded Room - Right': (0xeaac, 0x186280, False, 'in Swamp Palace'),
'Swamp Palace - Waterfall Room': (0xEAAF, False, 'in Swamp Palace'), 'Swamp Palace - Waterfall Room': (0xeaaf, 0x186283, False, 'in Swamp Palace'),
'Swamp Palace - Boss': (0x180154, False, 'with Arrghus'), 'Swamp Palace - Boss': (0x180154, 0x186342, False, 'with Arrghus'),
'Thieves\' Town - Big Key Chest': (0xEA04, False, 'in Thieves\' Town'), "Thieves' Town - Big Key Chest": (0xea04, 0x1861d8, False, "in Thieves' Town"),
'Thieves\' Town - Map Chest': (0xEA01, False, 'in Thieves\' Town'), "Thieves' Town - Map Chest": (0xea01, 0x1861d5, False, "in Thieves' Town"),
'Thieves\' Town - Compass Chest': (0xEA07, False, 'in Thieves\' Town'), "Thieves' Town - Compass Chest": (0xea07, 0x1861db, False, "in Thieves' Town"),
'Thieves\' Town - Ambush Chest': (0xEA0A, False, 'in Thieves\' Town'), "Thieves' Town - Ambush Chest": (0xea0a, 0x1861de, False, "in Thieves' Town"),
'Thieves\' Town - Attic': (0xEA0D, False, 'in Thieves\' Town'), "Thieves' Town - Attic": (0xea0d, 0x1861e1, False, "in Thieves' Town"),
'Thieves\' Town - Big Chest': (0xEA10, False, 'in Thieves\' Town'), "Thieves' Town - Big Chest": (0xea10, 0x1861e4, False, "in Thieves' Town"),
'Thieves\' Town - Blind\'s Cell': (0xEA13, False, 'in Thieves\' Town'), "Thieves' Town - Blind's Cell": (0xea13, 0x1861e7, False, "in Thieves' Town"),
'Thieves\' Town - Boss': (0x180156, False, 'with Blind'), "Thieves' Town - Boss": (0x180156, 0x186344, False, 'with Blind'),
'Skull Woods - Compass Chest': (0xE992, False, 'in Skull Woods'), 'Skull Woods - Compass Chest': (0xe992, 0x186166, False, 'in Skull Woods'),
'Skull Woods - Map Chest': (0xE99B, False, 'in Skull Woods'), 'Skull Woods - Map Chest': (0xe99b, 0x18616f, False, 'in Skull Woods'),
'Skull Woods - Big Chest': (0xE998, False, 'in Skull Woods'), 'Skull Woods - Big Chest': (0xe998, 0x18616c, False, 'in Skull Woods'),
'Skull Woods - Pot Prison': (0xE9A1, False, 'in Skull Woods'), 'Skull Woods - Pot Prison': (0xe9a1, 0x186175, False, 'in Skull Woods'),
'Skull Woods - Pinball Room': (0xE9C8, False, 'in Skull Woods'), 'Skull Woods - Pinball Room': (0xe9c8, 0x18619c, False, 'in Skull Woods'),
'Skull Woods - Big Key Chest': (0xE99E, False, 'in Skull Woods'), 'Skull Woods - Big Key Chest': (0xe99e, 0x186172, False, 'in Skull Woods'),
'Skull Woods - Bridge Room': (0xE9FE, False, 'near Mothula'), 'Skull Woods - Bridge Room': (0xe9fe, 0x1861d2, False, 'near Mothula'),
'Skull Woods - Boss': (0x180155, False, 'with Mothula'), 'Skull Woods - Boss': (0x180155, 0x186343, False, 'with Mothula'),
'Ice Palace - Compass Chest': (0xE9D4, False, 'in Ice Palace'), 'Ice Palace - Compass Chest': (0xe9d4, 0x1861a8, False, 'in Ice Palace'),
'Ice Palace - Freezor Chest': (0xE995, False, 'in Ice Palace'), 'Ice Palace - Freezor Chest': (0xe995, 0x186169, False, 'in Ice Palace'),
'Ice Palace - Big Chest': (0xE9AA, False, 'in Ice Palace'), 'Ice Palace - Big Chest': (0xe9aa, 0x18617e, False, 'in Ice Palace'),
'Ice Palace - Iced T Room': (0xE9E3, False, 'in Ice Palace'), 'Ice Palace - Iced T Room': (0xe9e3, 0x1861b7, False, 'in Ice Palace'),
'Ice Palace - Spike Room': (0xE9E0, False, 'in Ice Palace'), 'Ice Palace - Spike Room': (0xe9e0, 0x1861b4, False, 'in Ice Palace'),
'Ice Palace - Big Key Chest': (0xE9A4, False, 'in Ice Palace'), 'Ice Palace - Big Key Chest': (0xe9a4, 0x186178, False, 'in Ice Palace'),
'Ice Palace - Map Chest': (0xE9DD, False, 'in Ice Palace'), 'Ice Palace - Map Chest': (0xe9dd, 0x1861b1, False, 'in Ice Palace'),
'Ice Palace - Boss': (0x180157, False, 'with Kholdstare'), 'Ice Palace - Boss': (0x180157, 0x186345, False, 'with Kholdstare'),
'Misery Mire - Big Chest': (0xEA67, False, 'in Misery Mire'), 'Misery Mire - Big Chest': (0xea67, 0x18623b, False, 'in Misery Mire'),
'Misery Mire - Map Chest': (0xEA6A, False, 'in Misery Mire'), 'Misery Mire - Map Chest': (0xea6a, 0x18623e, False, 'in Misery Mire'),
'Misery Mire - Main Lobby': (0xEA5E, False, 'in Misery Mire'), 'Misery Mire - Main Lobby': (0xea5e, 0x186232, False, 'in Misery Mire'),
'Misery Mire - Bridge Chest': (0xEA61, False, 'in Misery Mire'), 'Misery Mire - Bridge Chest': (0xea61, 0x186235, False, 'in Misery Mire'),
'Misery Mire - Spike Chest': (0xE9DA, False, 'in Misery Mire'), 'Misery Mire - Spike Chest': (0xe9da, 0x1861ae, False, 'in Misery Mire'),
'Misery Mire - Compass Chest': (0xEA64, False, 'in Misery Mire'), 'Misery Mire - Compass Chest': (0xea64, 0x186238, False, 'in Misery Mire'),
'Misery Mire - Big Key Chest': (0xEA6D, False, 'in Misery Mire'), 'Misery Mire - Big Key Chest': (0xea6d, 0x186241, False, 'in Misery Mire'),
'Misery Mire - Boss': (0x180158, False, 'with Vitreous'), 'Misery Mire - Boss': (0x180158, 0x186346, False, 'with Vitreous'),
'Turtle Rock - Compass Chest': (0xEA22, False, 'in Turtle Rock'), 'Turtle Rock - Compass Chest': (0xea22, 0x1861f6, False, 'in Turtle Rock'),
'Turtle Rock - Roller Room - Left': (0xEA1C, False, 'in Turtle Rock'), 'Turtle Rock - Roller Room - Left': (0xea1c, 0x1861f0, False, 'in Turtle Rock'),
'Turtle Rock - Roller Room - Right': (0xEA1F, False, 'in Turtle Rock'), 'Turtle Rock - Roller Room - Right': (0xea1f, 0x1861f3, False, 'in Turtle Rock'),
'Turtle Rock - Chain Chomps': (0xEA16, False, 'in Turtle Rock'), 'Turtle Rock - Chain Chomps': (0xea16, 0x1861ea, False, 'in Turtle Rock'),
'Turtle Rock - Big Key Chest': (0xEA25, False, 'in Turtle Rock'), 'Turtle Rock - Big Key Chest': (0xea25, 0x1861f9, False, 'in Turtle Rock'),
'Turtle Rock - Big Chest': (0xEA19, False, 'in Turtle Rock'), 'Turtle Rock - Big Chest': (0xea19, 0x1861ed, False, 'in Turtle Rock'),
'Turtle Rock - Crystaroller Room': (0xEA34, False, 'in Turtle Rock'), 'Turtle Rock - Crystaroller Room': (0xea34, 0x186208, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Bottom Left': (0xEA31, False, 'in Turtle Rock'), 'Turtle Rock - Eye Bridge - Bottom Left': (0xea31, 0x186205, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Bottom Right': (0xEA2E, False, 'in Turtle Rock'), 'Turtle Rock - Eye Bridge - Bottom Right': (0xea2e, 0x186202, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Top Left': (0xEA2B, False, 'in Turtle Rock'), 'Turtle Rock - Eye Bridge - Top Left': (0xea2b, 0x1861ff, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Top Right': (0xEA28, False, 'in Turtle Rock'), 'Turtle Rock - Eye Bridge - Top Right': (0xea28, 0x1861fc, False, 'in Turtle Rock'),
'Turtle Rock - Boss': (0x180159, False, 'with Trinexx'), 'Turtle Rock - Boss': (0x180159, 0x186347, False, 'with Trinexx'),
'Palace of Darkness - Shooter Room': (0xEA5B, False, 'in Palace of Darkness'), 'Palace of Darkness - Shooter Room': (0xea5b, 0x18622f, False, 'in Palace of Darkness'),
'Palace of Darkness - The Arena - Bridge': (0xEA3D, False, 'in Palace of Darkness'), 'Palace of Darkness - The Arena - Bridge': (0xea3d, 0x186211, False, 'in Palace of Darkness'),
'Palace of Darkness - Stalfos Basement': (0xEA49, False, 'in Palace of Darkness'), 'Palace of Darkness - Stalfos Basement': (0xea49, 0x18621d, False, 'in Palace of Darkness'),
'Palace of Darkness - Big Key Chest': (0xEA37, False, 'in Palace of Darkness'), 'Palace of Darkness - Big Key Chest': (0xea37, 0x18620b, False, 'in Palace of Darkness'),
'Palace of Darkness - The Arena - Ledge': (0xEA3A, False, 'in Palace of Darkness'), 'Palace of Darkness - The Arena - Ledge': (0xea3a, 0x18620e, False, 'in Palace of Darkness'),
'Palace of Darkness - Map Chest': (0xEA52, False, 'in Palace of Darkness'), 'Palace of Darkness - Map Chest': (0xea52, 0x186226, False, 'in Palace of Darkness'),
'Palace of Darkness - Compass Chest': (0xEA43, False, 'in Palace of Darkness'), 'Palace of Darkness - Compass Chest': (0xea43, 0x186217, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Basement - Left': (0xEA4C, False, 'in Palace of Darkness'), 'Palace of Darkness - Dark Basement - Left': (0xea4c, 0x186220, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Basement - Right': (0xEA4F, False, 'in Palace of Darkness'), 'Palace of Darkness - Dark Basement - Right': (0xea4f, 0x186223, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Maze - Top': (0xEA55, False, 'in Palace of Darkness'), 'Palace of Darkness - Dark Maze - Top': (0xea55, 0x186229, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Maze - Bottom': (0xEA58, False, 'in Palace of Darkness'), 'Palace of Darkness - Dark Maze - Bottom': (0xea58, 0x18622c, False, 'in Palace of Darkness'),
'Palace of Darkness - Big Chest': (0xEA40, False, 'in Palace of Darkness'), 'Palace of Darkness - Big Chest': (0xea40, 0x186214, False, 'in Palace of Darkness'),
'Palace of Darkness - Harmless Hellway': (0xEA46, False, 'in Palace of Darkness'), 'Palace of Darkness - Harmless Hellway': (0xea46, 0x18621a, False, 'in Palace of Darkness'),
'Palace of Darkness - Boss': (0x180153, False, 'with Helmasaur King'), 'Palace of Darkness - Boss': (0x180153, 0x186341, False, 'with Helmasaur King'),
'Ganons Tower - Bob\'s Torch': (0x180161, False, 'in Ganon\'s Tower'), "Ganons Tower - Bob's Torch": (0x180161, 0x186363, False, "in Ganon's Tower"),
'Ganons Tower - Hope Room - Left': (0xEAD9, False, 'in Ganon\'s Tower'), 'Ganons Tower - Hope Room - Left': (0xead9, 0x1862ad, False, "in Ganon's Tower"),
'Ganons Tower - Hope Room - Right': (0xEADC, False, 'in Ganon\'s Tower'), 'Ganons Tower - Hope Room - Right': (0xeadc, 0x1862b0, False, "in Ganon's Tower"),
'Ganons Tower - Tile Room': (0xEAE2, False, 'in Ganon\'s Tower'), 'Ganons Tower - Tile Room': (0xeae2, 0x1862b6, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Top Left': (0xEAE5, False, 'in Ganon\'s Tower'), 'Ganons Tower - Compass Room - Top Left': (0xeae5, 0x1862b9, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Top Right': (0xEAE8, False, 'in Ganon\'s Tower'), 'Ganons Tower - Compass Room - Top Right': (0xeae8, 0x1862bc, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Bottom Left': (0xEAEB, False, 'in Ganon\'s Tower'), 'Ganons Tower - Compass Room - Bottom Left': (0xeaeb, 0x1862bf, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Bottom Right': (0xEAEE, False, 'in Ganon\'s Tower'), 'Ganons Tower - Compass Room - Bottom Right': (0xeaee, 0x1862c2, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Top Left': (0xEAB8, False, 'in Ganon\'s Tower'), 'Ganons Tower - DMs Room - Top Left': (0xeab8, 0x18628c, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Top Right': (0xEABB, False, 'in Ganon\'s Tower'), 'Ganons Tower - DMs Room - Top Right': (0xeabb, 0x18628f, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Bottom Left': (0xEABE, False, 'in Ganon\'s Tower'), 'Ganons Tower - DMs Room - Bottom Left': (0xeabe, 0x186292, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Bottom Right': (0xEAC1, False, 'in Ganon\'s Tower'), 'Ganons Tower - DMs Room - Bottom Right': (0xeac1, 0x186295, False, "in Ganon's Tower"),
'Ganons Tower - Map Chest': (0xEAD3, False, 'in Ganon\'s Tower'), 'Ganons Tower - Map Chest': (0xead3, 0x1862a7, False, "in Ganon's Tower"),
'Ganons Tower - Firesnake Room': (0xEAD0, False, 'in Ganon\'s Tower'), 'Ganons Tower - Firesnake Room': (0xead0, 0x1862a4, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Top Left': (0xEAC4, False, 'in Ganon\'s Tower'), 'Ganons Tower - Randomizer Room - Top Left': (0xeac4, 0x186298, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Top Right': (0xEAC7, False, 'in Ganon\'s Tower'), 'Ganons Tower - Randomizer Room - Top Right': (0xeac7, 0x18629b, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Bottom Left': (0xEACA, False, 'in Ganon\'s Tower'), 'Ganons Tower - Randomizer Room - Bottom Left': (0xeaca, 0x18629e, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Bottom Right': (0xEACD, False, 'in Ganon\'s Tower'), 'Ganons Tower - Randomizer Room - Bottom Right': (0xeacd, 0x1862a1, False, "in Ganon's Tower"),
'Ganons Tower - Bob\'s Chest': (0xEADF, False, 'in Ganon\'s Tower'), "Ganons Tower - Bob's Chest": (0xeadf, 0x1862b3, False, "in Ganon's Tower"),
'Ganons Tower - Big Chest': (0xEAD6, False, 'in Ganon\'s Tower'), 'Ganons Tower - Big Chest': (0xead6, 0x1862aa, False, "in Ganon's Tower"),
'Ganons Tower - Big Key Room - Left': (0xEAF4, False, 'in Ganon\'s Tower'), 'Ganons Tower - Big Key Room - Left': (0xeaf4, 0x1862c8, False, "in Ganon's Tower"),
'Ganons Tower - Big Key Room - Right': (0xEAF7, False, 'in Ganon\'s Tower'), 'Ganons Tower - Big Key Room - Right': (0xeaf7, 0x1862cb, False, "in Ganon's Tower"),
'Ganons Tower - Big Key Chest': (0xEAF1, False, 'in Ganon\'s Tower'), 'Ganons Tower - Big Key Chest': (0xeaf1, 0x1862c5, False, "in Ganon's Tower"),
'Ganons Tower - Mini Helmasaur Room - Left': (0xEAFD, False, 'atop Ganon\'s Tower'), 'Ganons Tower - Mini Helmasaur Room - Left': (0xeafd, 0x1862d1, False, "atop Ganon's Tower"),
'Ganons Tower - Mini Helmasaur Room - Right': (0xEB00, False, 'atop Ganon\'s Tower'), 'Ganons Tower - Mini Helmasaur Room - Right': (0xeb00, 0x1862d4, False, "atop Ganon's Tower"),
'Ganons Tower - Pre-Moldorm Chest': (0xEB03, False, 'atop Ganon\'s Tower'), 'Ganons Tower - Pre-Moldorm Chest': (0xeb03, 0x1862d7, False, "atop Ganon's Tower"),
'Ganons Tower - Validation Chest': (0xEB06, False, 'atop Ganon\'s Tower'), 'Ganons Tower - Validation Chest': (0xeb06, 0x1862da, False, "atop Ganon's Tower"),
'Ganon': (None, False, 'from me'), 'Ganon': (None, None, False, 'from me'),
'Agahnim 1': (None, False, 'from Ganon\'s wizardry form'), 'Agahnim 1': (None, None, False, 'from Ganon\'s wizardry form'),
'Agahnim 2': (None, False, 'from Ganon\'s wizardry form'), 'Agahnim 2': (None, None, False, 'from Ganon\'s wizardry form'),
'Floodgate': (None, False, None), 'Floodgate': (None, None, False, None),
'Frog': (None, False, None), 'Frog': (None, None, False, None),
'Missing Smith': (None, False, None), 'Missing Smith': (None, None, False, None),
'Dark Blacksmith Ruins': (None, False, None), 'Dark Blacksmith Ruins': (None, None, False, None),
'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], True, 'Eastern Palace'), 'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], None, True, 'Eastern Palace'),
'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], True, 'Desert Palace'), 'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], None, True, 'Desert Palace'),
'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], True, 'Tower of Hera'), 'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], None, True, 'Tower of Hera'),
'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], True, 'Palace of Darkness'), 'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], None, True, 'Palace of Darkness'),
'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], True, 'Swamp Palace'), 'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], None, True, 'Swamp Palace'),
'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], True, 'Thieves\' Town'), 'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], None, True, 'Thieves\' Town'),
'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], True, 'Skull Woods'), 'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], None, True, 'Skull Woods'),
'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], True, 'Ice Palace'), 'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], None, True, 'Ice Palace'),
'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], True, 'Misery Mire'), 'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], None, True, 'Misery Mire'),
'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], True, 'Turtle Rock')} 'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}

29
Main.py
View File

@ -4,6 +4,7 @@ from itertools import zip_longest
import json import json
import logging import logging
import os import os
import pickle
import random import random
import time import time
@ -16,7 +17,7 @@ from Rules import set_rules
from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items, balance_multiworld_progression from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items, balance_multiworld_progression
from ItemList import generate_itempool, difficulties, fill_prizes from ItemList import generate_itempool, difficulties, fill_prizes
from Utils import output_path from Utils import output_path, parse_names_string
__version__ = '0.6.3-pre' __version__ = '0.6.3-pre'
@ -120,16 +121,18 @@ def main(args, seed=None):
else: else:
sprite = None sprite = None
player_names = parse_names_string(args.names)
outfilebase = 'ER_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s_%s' % (world.logic, world.difficulty, world.difficulty_adjustments, world.mode, world.goal, "" if world.timer in ['none', 'display'] else "-" + world.timer, world.shuffle, world.algorithm, "-keysanity" if world.keysanity else "", "-retro" if world.retro else "", "-prog_" + world.progressive if world.progressive in ['off', 'random'] else "", "-nohints" if not world.hints else "", world.seed) outfilebase = 'ER_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s_%s' % (world.logic, world.difficulty, world.difficulty_adjustments, world.mode, world.goal, "" if world.timer in ['none', 'display'] else "-" + world.timer, world.shuffle, world.algorithm, "-keysanity" if world.keysanity else "", "-retro" if world.retro else "", "-prog_" + world.progressive if world.progressive in ['off', 'random'] else "", "-nohints" if not world.hints else "", world.seed)
use_enemizer = args.enemizercli and (args.shufflebosses != 'none' or args.shuffleenemies or args.enemy_health != 'default' or args.enemy_health != 'default' or args.enemy_damage or args.shufflepalette or args.shufflepots) use_enemizer = args.enemizercli and (args.shufflebosses != 'none' or args.shuffleenemies or args.enemy_health != 'default' or args.enemy_health != 'default' or args.enemy_damage or args.shufflepalette or args.shufflepots)
jsonout = {} jsonout = {}
if not args.suppress_rom: if not args.suppress_rom:
if world.players > 1: from MultiServer import MultiWorld
raise NotImplementedError("Multiworld rom writes have not been implemented") multidata = MultiWorld()
else: multidata.players = world.players
player = 1
for player in range(1, world.players + 1):
local_rom = None local_rom = None
if args.jsonout: if args.jsonout:
@ -147,17 +150,25 @@ def main(args, seed=None):
if use_enemizer: if use_enemizer:
enemizer_patch = get_enemizer_patch(world, player, rom, args.rom, args.enemizercli, args.shuffleenemies, args.enemy_health, args.enemy_damage, args.shufflepalette, args.shufflepots) enemizer_patch = get_enemizer_patch(world, player, rom, args.rom, args.enemizercli, args.shuffleenemies, args.enemy_health, args.enemy_damage, args.shufflepalette, args.shufflepots)
multidata.rom_names[player] = list(rom.name)
for location in world.get_filled_locations(player):
if type(location.address) is int:
multidata.locations[(location.address, player)] = (location.item.code, location.item.player)
if args.jsonout: if args.jsonout:
jsonout['patch'] = rom.patches jsonout[f'patch{player}'] = rom.patches
if use_enemizer: if use_enemizer:
jsonout['enemizer' % player] = enemizer_patch jsonout[f'enemizer{player}'] = enemizer_patch
else: else:
if use_enemizer: if use_enemizer:
local_rom.patch_enemizer(rom.patches, os.path.join(os.path.dirname(args.enemizercli), "enemizerBasePatch.json"), enemizer_patch) local_rom.patch_enemizer(rom.patches, os.path.join(os.path.dirname(args.enemizercli), "enemizerBasePatch.json"), enemizer_patch)
rom = local_rom rom = local_rom
apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite) apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite, player_names)
rom.write_to_file(output_path('%s.sfc' % outfilebase)) rom.write_to_file(output_path(f'{outfilebase}{f"_P{player}" if world.players > 1 else ""}{f"_{player_names[player]}" if player in player_names else ""}.sfc'))
with open(output_path('%s_multidata' % outfilebase), 'wb') as f:
pickle.dump(multidata, f, pickle.HIGHEST_PROTOCOL)
if args.create_spoiler and not args.jsonout: if args.create_spoiler and not args.jsonout:
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))

920
MultiClient.py Normal file
View File

@ -0,0 +1,920 @@
import argparse
import asyncio
import json
import logging
import re
import subprocess
import sys
import Items
import Regions
while True:
try:
import aioconsole
break
except ImportError:
aioconsole = None
print('Required python module "aioconsole" not found, press enter to install it')
input()
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'aioconsole'])
while True:
try:
import websockets
break
except ImportError:
websockets = None
print('Required python module "websockets" not found, press enter to install it')
input()
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'websockets'])
try:
import colorama
except ImportError:
colorama = None
class ReceivedItem:
def __init__(self, item, location, player_id, player_name):
self.item = item
self.location = location
self.player_id = player_id
self.player_name = player_name
class Context:
def __init__(self, snes_address, server_address, password, name, team, slot):
self.snes_address = snes_address
self.server_address = server_address
self.exit_event = asyncio.Event()
self.input_queue = asyncio.Queue()
self.input_requests = 0
self.snes_socket = None
self.snes_state = SNES_DISCONNECTED
self.snes_recv_queue = asyncio.Queue()
self.snes_request_lock = asyncio.Lock()
self.is_sd2snes = False
self.snes_write_buffer = []
self.server_task = None
self.socket = None
self.password = password
self.name = name
self.team = team
self.slot = slot
self.locations_checked = set()
self.items_received = []
self.last_rom = None
self.expected_rom = None
self.rom_confirmed = False
def color_code(*args):
codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
'magenta': 35, 'cyan': 36, 'white': 37 , 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
return '\033[' + ';'.join([str(codes[arg]) for arg in args]) + 'm'
def color(text, *args):
return color_code(*args) + text + color_code('reset')
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}
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
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 - Boss': (0x33, 0x800),
'Eastern Palace - Compass Chest': (0xa8, 0x10),
'Eastern Palace - Big Chest': (0xa9, 0x10),
'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 - Map Chest': (0x72, 0x10),
"Hyrule Castle - Zelda's Chest": (0x80, 0x10),
'Sewers - Dark Cross': (0x32, 0x10),
'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),
'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 - Big Chest': (0x36, 0x10),
'Swamp Palace - Compass Chest': (0x46, 0x10),
'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 - 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 - 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 - Bridge Room': (0x59, 0x10),
'Skull Woods - Boss': (0x29, 0x800),
'Ice Palace - Compass Chest': (0x2e, 0x10),
'Ice Palace - Freezor Chest': (0x7e, 0x10),
'Ice Palace - Big Chest': (0x9e, 0x10),
'Ice Palace - Iced T Room': (0xae, 0x10),
'Ice Palace - Spike Room': (0x5f, 0x10),
'Ice Palace - Big Key Chest': (0x1f, 0x10),
'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 - Spike Chest': (0xb3, 0x10),
'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 - Chain Chomps': (0xb6, 0x10),
'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 - 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 - 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 - 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 - Pre-Moldorm Chest': (0x3d, 0x40),
'Ganons Tower - Validation Chest': (0x4d, 0x10)}
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_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_misc = {'Bottle Merchant': (0x3c9, 0x2),
'Purple Chest': (0x3c9, 0x10),
"Link's Uncle": (0x3c6, 0x1),
'Hobo': (0x3c9, 0x1)}
SNES_DISCONNECTED = 0
SNES_CONNECTING = 1
SNES_CONNECTED = 2
SNES_ATTACHED = 3
async def snes_connect(ctx : Context, address = None):
if ctx.snes_socket is not None:
print('Already connected to snes')
return
ctx.snes_state = SNES_CONNECTING
recv_task = None
if address is None:
address = 'ws://' + ctx.snes_address
print("Connecting to QUsb2snes at %s ..." % address)
try:
ctx.snes_socket = await websockets.connect(address, ping_timeout=None, ping_interval=None)
ctx.snes_state = SNES_CONNECTED
DeviceList_Request = {
"Opcode" : "DeviceList",
"Space" : "SNES"
}
await ctx.snes_socket.send(json.dumps(DeviceList_Request))
reply = json.loads(await ctx.snes_socket.recv())
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
if not devices:
raise Exception('No device found')
print("Available devices:")
for id, device in enumerate(devices):
print("[%d] %s" % (id + 1, device))
device = None
while True:
print("Enter a number:")
choice = await console_input(ctx)
if choice is None:
raise Exception('Abort input')
if not choice.isdigit() or int(choice) < 1 or int(choice) > len(devices):
print("Invalid choice (%s)" % choice)
continue
device = devices[int(choice) - 1]
break
print("Attaching to " + device)
Attach_Request = {
"Opcode" : "Attach",
"Space" : "SNES",
"Operands" : [device]
}
await ctx.snes_socket.send(json.dumps(Attach_Request))
ctx.snes_state = SNES_ATTACHED
if 'SD2SNES'.lower() in device.lower() or (len(device) == 4 and device[:3] == 'COM'):
print("SD2SNES Detected")
ctx.is_sd2snes = True
await ctx.snes_socket.send(json.dumps({"Opcode" : "Info", "Space" : "SNES"}))
reply = json.loads(await ctx.snes_socket.recv())
if reply and 'Results' in reply:
print(reply['Results'])
else:
ctx.is_sd2snes = False
recv_task = asyncio.create_task(snes_recv_loop(ctx))
except Exception as e:
print("Error connecting to snes (%s)" % e)
if recv_task is not None:
if not ctx.snes_socket.closed:
await ctx.snes_socket.close()
else:
if ctx.snes_socket is not None:
if not ctx.snes_socket.closed:
await ctx.snes_socket.close()
ctx.snes_socket = None
ctx.snes_state = SNES_DISCONNECTED
async def snes_recv_loop(ctx : Context):
try:
async for msg in ctx.snes_socket:
ctx.snes_recv_queue.put_nowait(msg)
print("Snes disconnected, type /snes to reconnect")
except Exception as e:
print("Lost connection to the snes, type /snes to reconnect")
if type(e) is not websockets.ConnectionClosed:
logging.exception(e)
finally:
socket, ctx.snes_socket = ctx.snes_socket, None
if socket is not None and not socket.closed:
await socket.close()
ctx.snes_state = SNES_DISCONNECTED
ctx.snes_recv_queue = asyncio.Queue()
ctx.hud_message_queue = []
ctx.rom_confirmed = False
ctx.last_rom = None
async def snes_read(ctx : Context, address, size):
try:
await ctx.snes_request_lock.acquire()
if ctx.snes_state != SNES_ATTACHED or ctx.snes_socket is None or not ctx.snes_socket.open or ctx.snes_socket.closed:
return None
GetAddress_Request = {
"Opcode" : "GetAddress",
"Space" : "SNES",
"Operands" : [hex(address)[2:], hex(size)[2:]]
}
try:
await ctx.snes_socket.send(json.dumps(GetAddress_Request))
except websockets.ConnectionClosed:
return None
data = bytes()
while len(data) < size:
try:
data += await asyncio.wait_for(ctx.snes_recv_queue.get(), 5)
except asyncio.TimeoutError:
break
if len(data) != size:
print('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
if len(data):
print(str(data))
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
await ctx.snes_socket.close()
return None
return data
finally:
ctx.snes_request_lock.release()
async def snes_write(ctx : Context, write_list):
try:
await ctx.snes_request_lock.acquire()
if ctx.snes_state != SNES_ATTACHED or ctx.snes_socket is None or not ctx.snes_socket.open or ctx.snes_socket.closed:
return False
PutAddress_Request = {
"Opcode" : "PutAddress",
"Operands" : []
}
if ctx.is_sd2snes:
cmd = b'\x00\xE2\x20\x48\xEB\x48'
for address, data in write_list:
if (address < WRAM_START) or ((address + len(data)) > (WRAM_START + WRAM_SIZE)):
print("SD2SNES: Write out of range %s (%d)" % (hex(address), len(data)))
return False
for ptr, byte in enumerate(data, address + 0x7E0000 - WRAM_START):
cmd += b'\xA9' # LDA
cmd += bytes([byte])
cmd += b'\x8F' # STA.l
cmd += bytes([ptr & 0xFF, (ptr >> 8) & 0xFF, (ptr >> 16) & 0xFF])
cmd += b'\xA9\x00\x8F\x00\x2C\x00\x68\xEB\x68\x28\x6C\xEA\xFF\x08'
PutAddress_Request['Space'] = 'CMD'
PutAddress_Request['Operands'] = ["2C00", hex(len(cmd)-1)[2:], "2C00", "1"]
try:
if ctx.snes_socket is not None:
await ctx.snes_socket.send(json.dumps(PutAddress_Request))
if ctx.snes_socket is not None:
await ctx.snes_socket.send(cmd)
except websockets.ConnectionClosed:
return False
else:
PutAddress_Request['Space'] = 'SNES'
try:
#will pack those requests as soon as qusb2snes actually supports that for real
for address, data in write_list:
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
if ctx.snes_socket is not None:
await ctx.snes_socket.send(json.dumps(PutAddress_Request))
if ctx.snes_socket is not None:
await ctx.snes_socket.send(data)
except websockets.ConnectionClosed:
return False
return True
finally:
ctx.snes_request_lock.release()
def snes_buffered_write(ctx : Context, address, data):
if len(ctx.snes_write_buffer) > 0 and (ctx.snes_write_buffer[-1][0] + len(ctx.snes_write_buffer[-1][1])) == address:
ctx.snes_write_buffer[-1] = (ctx.snes_write_buffer[-1][0], ctx.snes_write_buffer[-1][1] + data)
else:
ctx.snes_write_buffer.append((address, data))
async def snes_flush_writes(ctx : Context):
if not ctx.snes_write_buffer:
return
await snes_write(ctx, ctx.snes_write_buffer)
ctx.snes_write_buffer = []
async def send_msgs(websocket, msgs):
if not websocket or not websocket.open or websocket.closed:
return
try:
await websocket.send(json.dumps(msgs))
except websockets.ConnectionClosed:
pass
async def server_loop(ctx : Context):
if ctx.socket is not None:
print('Already connected')
return
while not ctx.server_address:
print('Enter multiworld server address')
ctx.server_address = await console_input(ctx)
address = 'ws://' + ctx.server_address
print('Connecting to multiworld server at %s' % address)
try:
ctx.socket = await websockets.connect(address, ping_timeout=None, ping_interval=None)
print('Connected')
async for data in ctx.socket:
for msg in json.loads(data):
cmd, args = (msg[0], msg[1]) if len(msg) > 1 else (msg, None)
await process_server_cmd(ctx, cmd, args)
print('Disconnected from multiworld server, type /connect to reconnect')
except ConnectionRefusedError:
print('Connection refused by the multiworld server')
except (OSError, websockets.InvalidURI):
print('Failed to connect to the multiworld server')
except Exception as e:
print('Lost connection to the multiworld server, type /connect to reconnect')
if type(e) is not websockets.ConnectionClosed:
logging.exception(e)
finally:
ctx.name = None
ctx.team = None
ctx.slot = None
ctx.expected_rom = None
ctx.rom_confirmed = False
socket, ctx.socket = ctx.socket, None
if socket is not None and not socket.closed:
await socket.close()
ctx.server_task = None
async def process_server_cmd(ctx : Context, cmd, args):
if cmd == 'RoomInfo':
print('--------------------------------')
print('Room Information:')
print('--------------------------------')
if args['password']:
print('Password required')
print('%d players seed' % args['slots'])
if len(args['players']) < 1:
print('No player connected')
else:
args['players'].sort(key=lambda player: ('' if not player[1] else player[1].lower(), player[2]))
current_team = 0
print('Connected players:')
for name, team, slot in args['players']:
if team != current_team:
print(' Default team' if not team else ' Team: %s' % team)
current_team = team
print(' %s (Player %d)' % (name, slot))
await server_auth(ctx, args['password'])
if cmd == 'ConnectionRefused':
password_requested = False
if 'InvalidPassword' in args:
print('Invalid password')
ctx.password = None
password_requested = True
if 'InvalidName' in args:
print('Invalid name')
ctx.name = None
if 'NameAlreadyTaken' in args:
print('Name already taken')
ctx.name = None
if 'InvalidTeam' in args:
print('Invalid team name')
ctx.team = None
if 'InvalidSlot' in args:
print('Invalid player slot')
ctx.slot = None
if 'SlotAlreadyTaken' in args:
print('Player slot already in use for that team')
ctx.team = None
ctx.slot = None
await server_auth(ctx, password_requested)
if cmd == 'Connected':
ctx.expected_rom = args
if ctx.last_rom == ctx.expected_rom:
rom_confirmed(ctx)
if ctx.locations_checked:
await send_msgs(ctx.socket, [['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]]])
elif ctx.last_rom is not None:
raise Exception('Different ROM expected from server')
if cmd == 'ReceivedItems':
start_index, items = args
if start_index == 0:
ctx.items_received = []
elif start_index != len(ctx.items_received):
sync_msg = [['Sync']]
if ctx.locations_checked:
sync_msg.append(['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]])
await send_msgs(ctx.socket, sync_msg)
if start_index == len(ctx.items_received):
for item in items:
ctx.items_received.append(ReceivedItem(item[0], item[1], item[2], item[3]))
if cmd == 'ItemSent':
player_sent, player_recvd, item, location = args
item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.name else 'green')
player_sent = color(player_sent, 'yellow' if player_sent != ctx.name else 'magenta')
player_recvd = color(player_recvd, 'yellow' if player_recvd != ctx.name else 'magenta')
print('(%s) %s sent %s to %s (%s)' % (ctx.team if ctx.team else 'Team', player_sent, item, player_recvd, get_location_name_from_address(location)))
if cmd == 'Print':
print(args)
async def server_auth(ctx : Context, password_requested):
if password_requested and not ctx.password:
print('Enter the password required to join this game:')
ctx.password = await console_input(ctx)
while not ctx.name or not re.match(r'\w{1,10}', ctx.name):
print('Enter your name (10 characters):')
ctx.name = await console_input(ctx)
if not ctx.team:
print('Enter your team name (optional):')
ctx.team = await console_input(ctx)
if ctx.team == '': ctx.team = None
if not ctx.slot:
print('Choose your player slot (optional):')
slot = await console_input(ctx)
ctx.slot = int(slot) if slot.isdigit() else None
await send_msgs(ctx.socket, [['Connect', {'password': ctx.password, 'name': ctx.name, 'team': ctx.team, 'slot': ctx.slot}]])
async def console_input(ctx : Context):
ctx.input_requests += 1
return await ctx.input_queue.get()
async def console_loop(ctx : Context):
while not ctx.exit_event.is_set():
input = await aioconsole.ainput()
if ctx.input_requests > 0:
ctx.input_requests -= 1
ctx.input_queue.put_nowait(input)
continue
command = input.split()
if not command:
continue
if command[0] == '/exit':
ctx.exit_event.set()
if command[0] == '/installcolors' and 'colorama' not in sys.modules:
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'colorama'])
global colorama
import colorama
colorama.init()
if command[0] == '/snes':
asyncio.create_task(snes_connect(ctx, command[1] if len(command) > 1 else None))
if command[0] in ['/snes_close', '/snes_quit']:
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
await ctx.snes_socket.close()
async def disconnect():
if ctx.socket is not None and not ctx.socket.closed:
await ctx.socket.close()
if ctx.server_task is not None:
await ctx.server_task
async def connect():
await disconnect()
ctx.server_task = asyncio.create_task(server_loop(ctx))
if command[0] in ['/connect', '/reconnect']:
if len(command) > 1:
ctx.server_address = command[1]
asyncio.create_task(connect())
if command[0] == '/disconnect':
asyncio.create_task(disconnect())
if command[0][:1] != '/':
asyncio.create_task(send_msgs(ctx.socket, [['Say', input]]))
if command[0] == '/received':
print('Received items:')
for index, item in enumerate(ctx.items_received, 1):
print('%s from %s (%s) (%d/%d in list)' % (
color(get_item_name_from_id(item.item), 'red', 'bold'), color(item.player_name, 'yellow'),
get_location_name_from_address(item.location), index, len(ctx.items_received)))
if command[0] == '/missing':
for location in [k for k, v in Regions.location_table.items() if type(v[0]) is int]:
if location not in ctx.locations_checked:
print('Missing: ' + location)
if command[0] == '/getitem' and len(command) > 1:
item = input[9:]
item_id = Items.item_table[item][3] if item in Items.item_table else None
if type(item_id) is int and item_id in range(0x100):
print('Sending item: ' + item)
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item_id]))
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([0]))
else:
print('Invalid item: ' + item)
await snes_flush_writes(ctx)
def rom_confirmed(ctx : Context):
ctx.rom_confirmed = True
print('ROM hash Confirmed')
def get_item_name_from_id(code):
items = [k for k, i in Items.item_table.items() if type(i[3]) is int and i[3] == code]
return items[0] if items else 'Unknown item'
def get_location_name_from_address(address):
if type(address) is str:
return address
locs = [k for k, l in Regions.location_table.items() if type(l[0]) is int and l[0] == address]
return locs[0] if locs else 'Unknown location'
async def track_locations(ctx : Context, roomid, roomdata):
new_locations = []
def new_check(location):
ctx.locations_checked.add(location)
print("New check: %s (%d/216)" % (location, len(ctx.locations_checked)))
new_locations.append(Regions.location_table[location][0])
for location, (loc_roomid, loc_mask) in location_table_uw.items():
if location not in ctx.locations_checked and loc_roomid == roomid and (roomdata << 4) & loc_mask != 0:
new_check(location)
uw_begin = 0x129
uw_end = 0
uw_unchecked = {}
for location, (roomid, mask) in location_table_uw.items():
if location not in ctx.locations_checked:
uw_unchecked[location] = (roomid, mask)
uw_begin = min(uw_begin, roomid)
uw_end = max(uw_end, roomid + 1)
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, (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)
ow_begin = 0x82
ow_end = 0
ow_unchecked = {}
for location, screenid in location_table_ow.items():
if location not in ctx.locations_checked:
ow_unchecked[location] = screenid
ow_begin = min(ow_begin, screenid)
ow_end = max(ow_end, screenid + 1)
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, screenid in ow_unchecked.items():
if ow_data[screenid - ow_begin] & 0x40 != 0:
new_check(location)
if not all([location in ctx.locations_checked for location in location_table_npc.keys()]):
npc_data = await snes_read(ctx, SAVEDATA_START + 0x410, 2)
if npc_data is not None:
npc_value = npc_data[0] | (npc_data[1] << 8)
for location, mask in location_table_npc.items():
if npc_value & mask != 0 and location not in ctx.locations_checked:
new_check(location)
if not all([location in ctx.locations_checked for location in location_table_misc.keys()]):
misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4)
if misc_data is not None:
for location, (offset, mask) in location_table_misc.items():
assert(0x3c6 <= offset <= 0x3c9)
if misc_data[offset - 0x3c6] & mask != 0 and location not in ctx.locations_checked:
new_check(location)
await send_msgs(ctx.socket, [['LocationChecks', new_locations]])
async def game_watcher(ctx : Context):
while not ctx.exit_event.is_set():
await asyncio.sleep(2)
if not ctx.rom_confirmed:
rom = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
if rom is None or rom == bytes([0] * ROMNAME_SIZE):
continue
if list(rom) != ctx.last_rom:
ctx.last_rom = list(rom)
ctx.locations_checked = set()
if ctx.expected_rom is not None:
if ctx.last_rom != ctx.expected_rom:
print("Wrong ROM detected")
await ctx.snes_socket.close()
continue
else:
rom_confirmed(ctx)
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
if gamemode is None or gamemode[0] not in INGAME_MODES:
continue
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 7)
if data is None:
continue
recv_index = data[0] | (data[1] << 8)
assert(RECV_ITEM_ADDR == RECV_PROGRESS_ADDR + 2)
recv_item = data[2]
assert(ROOMID_ADDR == RECV_PROGRESS_ADDR + 4)
roomid = data[4] | (data[5] << 8)
assert(ROOMDATA_ADDR == RECV_PROGRESS_ADDR + 6)
roomdata = data[6]
await track_locations(ctx, roomid, roomdata)
if recv_index < len(ctx.items_received) and recv_item == 0:
item = ctx.items_received[recv_index]
print('Received %s from %s (%s) (%d/%d in list)' % (
color(get_item_name_from_id(item.item), 'red', 'bold'), color(item.player_name, 'yellow'),
get_location_name_from_address(item.location), recv_index + 1, len(ctx.items_received)))
recv_index += 1
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([item.player_id]))
await snes_flush_writes(ctx)
async def main():
parser = argparse.ArgumentParser()
parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.')
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
parser.add_argument('--name', default=None)
parser.add_argument('--team', default=None)
parser.add_argument('--slot', default=None, type=int)
args = parser.parse_args()
ctx = Context(args.snes, args.connect, args.password, args.name, args.team, args.slot)
input_task = asyncio.create_task(console_loop(ctx))
await snes_connect(ctx)
if ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx))
watcher_task = asyncio.create_task(game_watcher(ctx))
await ctx.exit_event.wait()
await watcher_task
if ctx.socket is not None and not ctx.socket.closed:
await ctx.socket.close()
if ctx.server_task is not None:
await ctx.server_task
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
await ctx.snes_socket.close()
while ctx.input_requests > 0:
ctx.input_queue.put_nowait(None)
ctx.input_requests -= 1
await input_task
if __name__ == '__main__':
if 'colorama' in sys.modules:
colorama.init()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.run_until_complete(asyncio.gather(*asyncio.Task.all_tasks()))
loop.close()
if 'colorama' in sys.modules:
colorama.deinit()

393
MultiServer.py Normal file
View File

@ -0,0 +1,393 @@
import aioconsole
import argparse
import asyncio
import functools
import json
import logging
import pickle
import re
import urllib.request
import websockets
import Items
import Regions
from MultiClient import ReceivedItem, get_item_name_from_id, get_location_name_from_address
class Client:
def __init__(self, socket):
self.socket = socket
self.auth = False
self.name = None
self.team = None
self.slot = None
self.send_index = 0
class MultiWorld:
def __init__(self):
self.players = None
self.rom_names = {}
self.locations = {}
class Context:
def __init__(self, host, port, password):
self.data_filename = None
self.save_filename = None
self.disable_save = False
self.world = MultiWorld()
self.host = host
self.port = port
self.password = password
self.server = None
self.clients = []
self.received_items = {}
def get_room_info(ctx : Context):
return {
'password': ctx.password is not None,
'slots': ctx.world.players,
'players': [(client.name, client.team, client.slot) for client in ctx.clients if client.auth]
}
def same_name(lhs, rhs):
return lhs.lower() == rhs.lower()
def same_team(lhs, rhs):
return (type(lhs) is type(rhs)) and ((not lhs and not rhs) or (lhs.lower() == rhs.lower()))
async def send_msgs(websocket, msgs):
if not websocket or not websocket.open or websocket.closed:
return
try:
await websocket.send(json.dumps(msgs))
except websockets.ConnectionClosed:
pass
def broadcast_all(ctx : Context, msgs):
for client in ctx.clients:
if client.auth:
asyncio.create_task(send_msgs(client.socket, msgs))
def broadcast_team(ctx : Context, team, msgs):
for client in ctx.clients:
if client.auth and same_team(client.team, team):
asyncio.create_task(send_msgs(client.socket, msgs))
def notify_all(ctx : Context, text):
print("Notice (all): %s" % text)
broadcast_all(ctx, [['Print', text]])
def notify_team(ctx : Context, team : str, text : str):
print("Team notice (%s): %s" % ("Default" if not team else team, text))
broadcast_team(ctx, team, [['Print', text]])
def notify_client(client : Client, text : str):
if not client.auth:
return
print("Player notice (%s): %s" % (client.name, text))
asyncio.create_task(send_msgs(client.socket, [['Print', text]]))
async def server(websocket, path, ctx : Context):
client = Client(websocket)
ctx.clients.append(client)
try:
await on_client_connected(ctx, client)
async for data in websocket:
for msg in json.loads(data):
if len(msg) == 1:
cmd = msg
args = None
else:
cmd = msg[0]
args = msg[1]
await process_client_cmd(ctx, client, cmd, args)
except Exception as e:
if type(e) is not websockets.ConnectionClosed:
logging.exception(e)
finally:
await on_client_disconnected(ctx, client)
ctx.clients.remove(client)
async def on_client_connected(ctx : Context, client : Client):
await send_msgs(client.socket, [['RoomInfo', get_room_info(ctx)]])
async def on_client_disconnected(ctx : Context, client : Client):
if client.auth:
await on_client_left(ctx, client)
async def on_client_joined(ctx : Context, client : Client):
notify_all(ctx, "%s has joined the game as player %d for %s" % (client.name, client.slot, "the default team" if not client.team else "team %s" % client.team))
async def on_client_left(ctx : Context, client : Client):
notify_all(ctx, "%s (Player %d, %s) has left the game" % (client.name, client.slot, "Default team" if not client.team else "Team %s" % client.team))
def get_connected_players_string(ctx : Context):
auth_clients = [c for c in ctx.clients if c.auth]
if not auth_clients:
return 'No player connected'
auth_clients.sort(key=lambda c: ('' if not c.team else c.team.lower(), c.slot))
current_team = 0
text = ''
for c in auth_clients:
if c.team != current_team:
text += '::' + ('default team' if not c.team else c.team) + ':: '
current_team = c.team
text += '%d:%s ' % (c.slot, c.name)
return 'Connected players: ' + text[:-1]
def get_player_name_in_team(ctx : Context, team, slot):
for client in ctx.clients:
if client.auth and same_team(team, client.team) and client.slot == slot:
return client.name
return "Player %d" % slot
def get_client_from_name(ctx : Context, name):
for client in ctx.clients:
if client.auth and same_name(name, client.name):
return client
return None
def get_received_items(ctx : Context, team, player):
for (c_team, c_id), items in ctx.received_items.items():
if c_id == player and same_team(c_team, team):
return items
ctx.received_items[(team, player)] = []
return ctx.received_items[(team, player)]
def tuplize_received_items(items):
return [(item.item, item.location, item.player_id, item.player_name) for item in items]
def send_new_items(ctx : Context):
for client in ctx.clients:
if not client.auth:
continue
items = get_received_items(ctx, client.team, client.slot)
if len(items) > client.send_index:
asyncio.create_task(send_msgs(client.socket, [['ReceivedItems', (client.send_index, tuplize_received_items(items)[client.send_index:])]]))
client.send_index = len(items)
def forfeit_player(ctx : Context, team, slot, name):
all_locations = [values[0] for values in Regions.location_table.values() if type(values[0]) is int]
notify_all(ctx, "%s (Player %d) in team %s has forfeited" % (name, slot, team if team else 'default'))
register_location_checks(ctx, name, team, slot, all_locations)
def register_location_checks(ctx : Context, name, team, slot, locations):
found_items = False
for location in locations:
if (location, slot) in ctx.world.locations:
target_item, target_player = ctx.world.locations[(location, slot)]
if target_player != slot:
found = False
recvd_items = get_received_items(ctx, team, target_player)
for recvd_item in recvd_items:
if recvd_item.location == location and recvd_item.player_id == slot:
found = True
break
if not found:
new_item = ReceivedItem(target_item, location, slot, name)
recvd_items.append(new_item)
target_player_name = get_player_name_in_team(ctx, team, target_player)
broadcast_team(ctx, team, [['ItemSent', (name, target_player_name, target_item, location)]])
print('(%s) %s sent %s to %s (%s)' % (team if team else 'Team', name, get_item_name_from_id(target_item), target_player_name, get_location_name_from_address(location)))
found_items = True
send_new_items(ctx)
if found_items and not ctx.disable_save:
try:
with open(ctx.save_filename, "wb") as f:
pickle.dump((ctx.world.players, ctx.world.rom_names, ctx.received_items), f, pickle.HIGHEST_PROTOCOL)
except Exception as e:
logging.exception(e)
async def process_client_cmd(ctx : Context, client : Client, cmd, args):
if type(cmd) is not str:
await send_msgs(client.socket, [['InvalidCmd']])
return
if cmd == 'Connect':
if not args or type(args) is not dict or \
'password' not in args or type(args['password']) not in [str, type(None)] or \
'name' not in args or type(args['name']) is not str or \
'team' not in args or type(args['team']) not in [str, type(None)] or \
'slot' not in args or type(args['slot']) not in [int, type(None)]:
await send_msgs(client.socket, [['InvalidArguments', 'Connect']])
return
errors = set()
if ctx.password is not None and ('password' not in args or args['password'] != ctx.password):
errors.add('InvalidPassword')
if 'name' not in args or not args['name'] or not re.match(r'\w{1,10}', args['name']):
errors.add('InvalidName')
elif any([same_name(c.name, args['name']) for c in ctx.clients if c.auth]):
errors.add('NameAlreadyTaken')
else:
client.name = args['name']
if 'team' in args and args['team'] is not None and not re.match(r'\w{1,15}', args['team']):
errors.add('InvalidTeam')
else:
client.team = args['team'] if 'team' in args else None
if 'slot' in args and any([c.slot == args['slot'] for c in ctx.clients if c.auth and same_team(c.team, client.team)]):
errors.add('SlotAlreadyTaken')
elif 'slot' not in args or not args['slot']:
for slot in range(1, ctx.world.players + 1):
if slot not in [c.slot for c in ctx.clients if c.auth and same_team(c.team, client.team)]:
client.slot = slot
break
elif slot == ctx.world.players:
errors.add('SlotAlreadyTaken')
elif args['slot'] not in range(1, ctx.world.players + 1):
errors.add('InvalidSlot')
else:
client.slot = args['slot']
if errors:
client.name = None
client.team = None
client.slot = None
await send_msgs(client.socket, [['ConnectionRefused', list(errors)]])
else:
client.auth = True
reply = [['Connected', ctx.world.rom_names[client.slot]]]
items = get_received_items(ctx, client.team, client.slot)
if items:
reply.append(['ReceivedItems', (0, tuplize_received_items(items))])
client.send_index = len(items)
await send_msgs(client.socket, reply)
await on_client_joined(ctx, client)
if not client.auth:
return
if cmd == 'Sync':
items = get_received_items(ctx, client.team, client.slot)
if items:
client.send_index = len(items)
await send_msgs(client.socket, ['ReceivedItems', (0, tuplize_received_items(items))])
if cmd == 'LocationChecks':
if type(args) is not list:
await send_msgs(client.socket, [['InvalidArguments', 'LocationChecks']])
return
register_location_checks(ctx, client.name, client.team, client.slot, args)
if cmd == 'Say':
if type(args) is not str or not args.isprintable():
await send_msgs(client.socket, [['InvalidArguments', 'Say']])
return
notify_all(ctx, client.name + ': ' + args)
if args[:8] == '!players':
notify_all(ctx, get_connected_players_string(ctx))
if args[:8] == '!forfeit':
forfeit_player(ctx, client.team, client.slot, client.name)
def set_password(ctx : Context, password):
ctx.password = password
print('Password set to ' + password if password is not None else 'Password disabled')
async def console(ctx : Context):
while True:
input = await aioconsole.ainput()
command = input.split()
if not command:
continue
if command[0] == '/exit':
ctx.server.ws_server.close()
break
if command[0] == '/players':
print(get_connected_players_string(ctx))
if command[0] == '/password':
set_password(ctx, command[1] if len(command) > 1 else None)
if command[0] == '/kick' and len(command) > 1:
client = get_client_from_name(ctx, command[1])
if client and client.socket and not client.socket.closed:
await client.socket.close()
if command[0] == '/forfeitslot' and len(command) == 3 and command[2].isdigit():
team = command[1] if command[1] != 'default' else None
slot = int(command[2])
name = get_player_name_in_team(ctx, team, slot)
forfeit_player(ctx, team, slot, name)
if command[0] == '/forfeitplayer' and len(command) > 1:
client = get_client_from_name(ctx, command[1])
if client:
forfeit_player(ctx, client.team, client.slot, client.name)
if command[0] == '/senditem' and len(command) > 2:
[(player, item)] = re.findall(r'\S* (\S*) (.*)', input)
if item in Items.item_table:
client = get_client_from_name(ctx, player)
if client:
new_item = ReceivedItem(Items.item_table[item][3], "cheat console", 0, "server")
get_received_items(ctx, client.team, client.slot).append(new_item)
notify_all(ctx, 'Cheat console: sending "' + item + '" to ' + client.name)
send_new_items(ctx)
else:
print("Unknown item: " + item)
if command[0][0] != '/':
notify_all(ctx, '[Server]: ' + input)
async def main():
parser = argparse.ArgumentParser()
parser.add_argument('--host', default=None)
parser.add_argument('--port', default=38281, type=int)
parser.add_argument('--password', default=None)
parser.add_argument('--multidata', default=None)
parser.add_argument('--savefile', default=None)
parser.add_argument('--disable_save', default=False, action='store_true')
args = parser.parse_args()
ctx = Context(args.host, args.port, args.password)
ctx.data_filename = args.multidata
try:
if not ctx.data_filename:
import tkinter
import tkinter.filedialog
root = tkinter.Tk()
root.withdraw()
ctx.data_filename = tkinter.filedialog.askopenfilename(filetypes=(("Multiworld data","*multidata"),))
with open(ctx.data_filename, 'rb') as f:
ctx.world = pickle.load(f)
except Exception as e:
print('Failed to read multiworld data (%s)' % e)
return
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8') if not ctx.host else ctx.host
print('Hosting game of %d players (%s) at %s:%d' % (ctx.world.players, 'No password' if not ctx.password else 'Password: %s' % ctx.password, ip, ctx.port))
ctx.disable_save = args.disable_save
if not ctx.disable_save:
if not ctx.save_filename:
ctx.save_filename = (ctx.data_filename[:-9] if ctx.data_filename[-9:] == 'multidata' else (ctx.data_filename + '_')) + 'multisave'
try:
with open(ctx.save_filename, 'rb') as f:
players, rom_names, received_items = pickle.load(f)
if players != ctx.world.players or rom_names != ctx.world.rom_names:
raise Exception('Save file mismatch, will start a new game')
ctx.received_items = received_items
print('Loaded save file with %d received items for %d players' % (sum([len(p) for p in received_items.values()]), len(received_items)))
except FileNotFoundError:
print('No save data found, starting a new game')
except Exception as e:
print(e)
ctx.server = websockets.serve(functools.partial(server,ctx=ctx), ctx.host, ctx.port, ping_timeout=None, ping_interval=None)
await ctx.server
await console(ctx)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.run_until_complete(asyncio.gather(*asyncio.Task.all_tasks()))
loop.close()

View File

@ -10,7 +10,7 @@ import sys
from BaseClasses import World from BaseClasses import World
from Regions import create_regions from Regions import create_regions
from EntranceShuffle import link_entrances, connect_entrance, connect_two_way, connect_exit from EntranceShuffle import link_entrances, connect_entrance, connect_two_way, connect_exit
from Rom import patch_rom, LocalRom, Sprite, write_string_to_rom from Rom import patch_rom, LocalRom, Sprite, write_string_to_rom, apply_rom_settings
from Rules import set_rules from Rules import set_rules
from Dungeons import create_dungeons from Dungeons import create_dungeons
from Items import ItemFactory from Items import ItemFactory
@ -74,7 +74,9 @@ def main(args):
sprite = None sprite = None
rom = LocalRom(args.rom) rom = LocalRom(args.rom)
patch_rom(world, 1, rom, args.heartbeep, args.heartcolor, sprite) patch_rom(world, 1, rom)
apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite)
for textname, texttype, text in text_patches: for textname, texttype, text in text_patches:
if texttype == 'text': if texttype == 'text':

View File

@ -331,8 +331,8 @@ def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None
for exit in exits: for exit in exits:
ret.exits.append(Entrance(player, exit, ret)) ret.exits.append(Entrance(player, exit, ret))
for location in locations: for location in locations:
address, crystal, hint_text = location_table[location] address, player_address, crystal, hint_text = location_table[location]
ret.locations.append(Location(player, location, address, crystal, hint_text, ret)) ret.locations.append(Location(player, location, address, crystal, hint_text, ret, player_address))
return ret return ret
def mark_light_world_regions(world): def mark_light_world_regions(world):
@ -397,236 +397,236 @@ default_shop_contents = {
'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)], 'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)],
} }
location_table = {'Mushroom': (0x180013, False, 'in the woods'), location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
'Bottle Merchant': (0x2eb18, False, 'with a merchant'), 'Bottle Merchant': (0x2eb18, 0x186339, False, 'with a merchant'),
'Flute Spot': (0x18014a, False, 'underground'), 'Flute Spot': (0x18014a, 0x18633d, False, 'underground'),
'Sunken Treasure': (0x180145, False, 'underwater'), 'Sunken Treasure': (0x180145, 0x186354, False, 'underwater'),
'Purple Chest': (0x33d68, False, 'from a box'), 'Purple Chest': (0x33d68, 0x186359, False, 'from a box'),
"Blind's Hideout - Top": (0xeb0f, False, 'in a basement'), "Blind's Hideout - Top": (0xeb0f, 0x1862e3, False, 'in a basement'),
"Blind's Hideout - Left": (0xeb12, False, 'in a basement'), "Blind's Hideout - Left": (0xeb12, 0x1862e6, False, 'in a basement'),
"Blind's Hideout - Right": (0xeb15, False, 'in a basement'), "Blind's Hideout - Right": (0xeb15, 0x1862e9, False, 'in a basement'),
"Blind's Hideout - Far Left": (0xeb18, False, 'in a basement'), "Blind's Hideout - Far Left": (0xeb18, 0x1862ec, False, 'in a basement'),
"Blind's Hideout - Far Right": (0xeb1b, False, 'in a basement'), "Blind's Hideout - Far Right": (0xeb1b, 0x1862ef, False, 'in a basement'),
"Link's Uncle": (0x2df45, False, 'with your uncle'), "Link's Uncle": (0x2df45, 0x18635f, False, 'with your uncle'),
'Secret Passage': (0xe971, False, 'near your uncle'), 'Secret Passage': (0xe971, 0x186145, False, 'near your uncle'),
'King Zora': (0xee1c3, False, 'at a high price'), 'King Zora': (0xee1c3, 0x186360, False, 'at a high price'),
"Zora's Ledge": (0x180149, False, 'near Zora'), "Zora's Ledge": (0x180149, 0x186358, False, 'near Zora'),
'Waterfall Fairy - Left': (0xe9b0, False, 'near a fairy'), 'Waterfall Fairy - Left': (0xe9b0, 0x186184, False, 'near a fairy'),
'Waterfall Fairy - Right': (0xe9d1, False, 'near a fairy'), 'Waterfall Fairy - Right': (0xe9d1, 0x1861a5, False, 'near a fairy'),
"King's Tomb": (0xe97a, False, 'alone in a cave'), "King's Tomb": (0xe97a, 0x18614e, False, 'alone in a cave'),
'Floodgate Chest': (0xe98c, False, 'in the dam'), 'Floodgate Chest': (0xe98c, 0x186160, False, 'in the dam'),
"Link's House": (0xe9bc, False, 'in your home'), "Link's House": (0xe9bc, 0x186190, False, 'in your home'),
'Kakariko Tavern': (0xe9ce, False, 'in the bar'), 'Kakariko Tavern': (0xe9ce, 0x1861a2, False, 'in the bar'),
'Chicken House': (0xe9e9, False, 'near poultry'), 'Chicken House': (0xe9e9, 0x1861bd, False, 'near poultry'),
"Aginah's Cave": (0xe9f2, False, 'with Aginah'), "Aginah's Cave": (0xe9f2, 0x1861c6, False, 'with Aginah'),
"Sahasrahla's Hut - Left": (0xea82, False, 'near the elder'), "Sahasrahla's Hut - Left": (0xea82, 0x186256, False, 'near the elder'),
"Sahasrahla's Hut - Middle": (0xea85, False, 'near the elder'), "Sahasrahla's Hut - Middle": (0xea85, 0x186259, False, 'near the elder'),
"Sahasrahla's Hut - Right": (0xea88, False, 'near the elder'), "Sahasrahla's Hut - Right": (0xea88, 0x18625c, False, 'near the elder'),
'Sahasrahla': (0x2f1fc, False, 'with the elder'), 'Sahasrahla': (0x2f1fc, 0x186365, False, 'with the elder'),
'Kakariko Well - Top': (0xea8e, False, 'in a well'), 'Kakariko Well - Top': (0xea8e, 0x186262, False, 'in a well'),
'Kakariko Well - Left': (0xea91, False, 'in a well'), 'Kakariko Well - Left': (0xea91, 0x186265, False, 'in a well'),
'Kakariko Well - Middle': (0xea94, False, 'in a well'), 'Kakariko Well - Middle': (0xea94, 0x186268, False, 'in a well'),
'Kakariko Well - Right': (0xea97, False, 'in a well'), 'Kakariko Well - Right': (0xea97, 0x18626b, False, 'in a well'),
'Kakariko Well - Bottom': (0xea9a, False, 'in a well'), 'Kakariko Well - Bottom': (0xea9a, 0x18626e, False, 'in a well'),
'Blacksmith': (0x18002a, False, 'with the smith'), 'Blacksmith': (0x18002a, 0x186366, False, 'with the smith'),
'Magic Bat': (0x180015, False, 'with the bat'), 'Magic Bat': (0x180015, 0x18635e, False, 'with the bat'),
'Sick Kid': (0x339cf, False, 'with the sick'), 'Sick Kid': (0x339cf, 0x186367, False, 'with the sick'),
'Hobo': (0x33e7d, False, 'with the hobo'), 'Hobo': (0x33e7d, 0x186368, False, 'with the hobo'),
'Lost Woods Hideout': (0x180000, False, 'near a thief'), 'Lost Woods Hideout': (0x180000, 0x186348, False, 'near a thief'),
'Lumberjack Tree': (0x180001, False, 'in a hole'), 'Lumberjack Tree': (0x180001, 0x186349, False, 'in a hole'),
'Cave 45': (0x180003, False, 'alone in a cave'), 'Cave 45': (0x180003, 0x18634b, False, 'alone in a cave'),
'Graveyard Cave': (0x180004, False, 'alone in a cave'), 'Graveyard Cave': (0x180004, 0x18634c, False, 'alone in a cave'),
'Checkerboard Cave': (0x180005, False, 'alone in a cave'), 'Checkerboard Cave': (0x180005, 0x18634d, False, 'alone in a cave'),
'Mini Moldorm Cave - Far Left': (0xeb42, False, 'near Moldorms'), 'Mini Moldorm Cave - Far Left': (0xeb42, 0x186316, False, 'near Moldorms'),
'Mini Moldorm Cave - Left': (0xeb45, False, 'near Moldorms'), 'Mini Moldorm Cave - Left': (0xeb45, 0x186319, False, 'near Moldorms'),
'Mini Moldorm Cave - Right': (0xeb48, False, 'near Moldorms'), 'Mini Moldorm Cave - Right': (0xeb48, 0x18631c, False, 'near Moldorms'),
'Mini Moldorm Cave - Far Right': (0xeb4b, False, 'near Moldorms'), 'Mini Moldorm Cave - Far Right': (0xeb4b, 0x18631f, False, 'near Moldorms'),
'Mini Moldorm Cave - Generous Guy': (0x180010, False, 'near Moldorms'), 'Mini Moldorm Cave - Generous Guy': (0x180010, 0x18635a, False, 'near Moldorms'),
'Ice Rod Cave': (0xeb4e, False, 'in a frozen cave'), 'Ice Rod Cave': (0xeb4e, 0x186322, False, 'in a frozen cave'),
'Bonk Rock Cave': (0xeb3f, False, 'alone in a cave'), 'Bonk Rock Cave': (0xeb3f, 0x186313, False, 'alone in a cave'),
'Library': (0x180012, False, 'near books'), 'Library': (0x180012, 0x18635c, False, 'near books'),
'Potion Shop': (0x180014, False, 'near potions'), 'Potion Shop': (0x180014, 0x18635d, False, 'near potions'),
'Lake Hylia Island': (0x180144, False, 'on an island'), 'Lake Hylia Island': (0x180144, 0x186353, False, 'on an island'),
'Maze Race': (0x180142, False, 'at the race'), 'Maze Race': (0x180142, 0x186351, False, 'at the race'),
'Desert Ledge': (0x180143, False, 'in the desert'), 'Desert Ledge': (0x180143, 0x186352, False, 'in the desert'),
'Desert Palace - Big Chest': (0xe98f, False, 'in Desert Palace'), 'Desert Palace - Big Chest': (0xe98f, 0x186163, False, 'in Desert Palace'),
'Desert Palace - Torch': (0x180160, False, 'in Desert Palace'), 'Desert Palace - Torch': (0x180160, 0x186362, False, 'in Desert Palace'),
'Desert Palace - Map Chest': (0xe9b6, False, 'in Desert Palace'), 'Desert Palace - Map Chest': (0xe9b6, 0x18618a, False, 'in Desert Palace'),
'Desert Palace - Compass Chest': (0xe9cb, False, 'in Desert Palace'), 'Desert Palace - Compass Chest': (0xe9cb, 0x18619f, False, 'in Desert Palace'),
'Desert Palace - Big Key Chest': (0xe9c2, False, 'in Desert Palace'), 'Desert Palace - Big Key Chest': (0xe9c2, 0x186196, False, 'in Desert Palace'),
'Desert Palace - Boss': (0x180151, False, 'with Lanmolas'), 'Desert Palace - Boss': (0x180151, 0x18633f, False, 'with Lanmolas'),
'Eastern Palace - Compass Chest': (0xe977, False, 'in Eastern Palace'), 'Eastern Palace - Compass Chest': (0xe977, 0x18614b, False, 'in Eastern Palace'),
'Eastern Palace - Big Chest': (0xe97d, False, 'in Eastern Palace'), 'Eastern Palace - Big Chest': (0xe97d, 0x186151, False, 'in Eastern Palace'),
'Eastern Palace - Cannonball Chest': (0xe9b3, False, 'in Eastern Palace'), 'Eastern Palace - Cannonball Chest': (0xe9b3, 0x186187, False, 'in Eastern Palace'),
'Eastern Palace - Big Key Chest': (0xe9b9, False, 'in Eastern Palace'), 'Eastern Palace - Big Key Chest': (0xe9b9, 0x18618d, False, 'in Eastern Palace'),
'Eastern Palace - Map Chest': (0xe9f5, False, 'in Eastern Palace'), 'Eastern Palace - Map Chest': (0xe9f5, 0x1861c9, False, 'in Eastern Palace'),
'Eastern Palace - Boss': (0x180150, False, 'with the Armos'), 'Eastern Palace - Boss': (0x180150, 0x18633e, False, 'with the Armos'),
'Master Sword Pedestal': (0x289b0, False, 'at the pedestal'), 'Master Sword Pedestal': (0x289b0, 0x186369, False, 'at the pedestal'),
'Hyrule Castle - Boomerang Chest': (0xe974, False, 'in Hyrule Castle'), 'Hyrule Castle - Boomerang Chest': (0xe974, 0x186148, False, 'in Hyrule Castle'),
'Hyrule Castle - Map Chest': (0xeb0c, False, 'in Hyrule Castle'), 'Hyrule Castle - Map Chest': (0xeb0c, 0x1862e0, False, 'in Hyrule Castle'),
"Hyrule Castle - Zelda's Chest": (0xeb09, False, 'in Hyrule Castle'), "Hyrule Castle - Zelda's Chest": (0xeb09, 0x1862dd, False, 'in Hyrule Castle'),
'Sewers - Dark Cross': (0xe96e, False, 'in the sewers'), 'Sewers - Dark Cross': (0xe96e, 0x186142, False, 'in the sewers'),
'Sewers - Secret Room - Left': (0xeb5d, False, 'in the sewers'), 'Sewers - Secret Room - Left': (0xeb5d, 0x186331, False, 'in the sewers'),
'Sewers - Secret Room - Middle': (0xeb60, False, 'in the sewers'), 'Sewers - Secret Room - Middle': (0xeb60, 0x186334, False, 'in the sewers'),
'Sewers - Secret Room - Right': (0xeb63, False, 'in the sewers'), 'Sewers - Secret Room - Right': (0xeb63, 0x186337, False, 'in the sewers'),
'Sanctuary': (0xea79, False, 'in Sanctuary'), 'Sanctuary': (0xea79, 0x18624d, False, 'in Sanctuary'),
'Castle Tower - Room 03': (0xeab5, False, 'in Castle Tower'), 'Castle Tower - Room 03': (0xeab5, 0x186289, False, 'in Castle Tower'),
'Castle Tower - Dark Maze': (0xeab2, False, 'in Castle Tower'), 'Castle Tower - Dark Maze': (0xeab2, 0x186286, False, 'in Castle Tower'),
'Old Man': (0xf69fa, False, 'with the old man'), 'Old Man': (0xf69fa, 0x186364, False, 'with the old man'),
'Spectacle Rock Cave': (0x180002, False, 'alone in a cave'), 'Spectacle Rock Cave': (0x180002, 0x18634a, False, 'alone in a cave'),
'Paradox Cave Lower - Far Left': (0xeb2a, False, 'in a cave with seven chests'), 'Paradox Cave Lower - Far Left': (0xeb2a, 0x1862fe, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Left': (0xeb2d, False, 'in a cave with seven chests'), 'Paradox Cave Lower - Left': (0xeb2d, 0x186301, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Right': (0xeb30, False, 'in a cave with seven chests'), 'Paradox Cave Lower - Right': (0xeb30, 0x186304, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Far Right': (0xeb33, False, 'in a cave with seven chests'), 'Paradox Cave Lower - Far Right': (0xeb33, 0x186307, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Middle': (0xeb36, False, 'in a cave with seven chests'), 'Paradox Cave Lower - Middle': (0xeb36, 0x18630a, False, 'in a cave with seven chests'),
'Paradox Cave Upper - Left': (0xeb39, False, 'in a cave with seven chests'), 'Paradox Cave Upper - Left': (0xeb39, 0x18630d, False, 'in a cave with seven chests'),
'Paradox Cave Upper - Right': (0xeb3c, False, 'in a cave with seven chests'), 'Paradox Cave Upper - Right': (0xeb3c, 0x186310, False, 'in a cave with seven chests'),
'Spiral Cave': (0xe9bf, False, 'in spiral cave'), 'Spiral Cave': (0xe9bf, 0x186193, False, 'in spiral cave'),
'Ether Tablet': (0x180016, False, 'at a monolith'), 'Ether Tablet': (0x180016, 0x18633b, False, 'at a monolith'),
'Spectacle Rock': (0x180140, False, 'atop a rock'), 'Spectacle Rock': (0x180140, 0x18634f, False, 'atop a rock'),
'Tower of Hera - Basement Cage': (0x180162, False, 'in Tower of Hera'), 'Tower of Hera - Basement Cage': (0x180162, 0x18633a, False, 'in Tower of Hera'),
'Tower of Hera - Map Chest': (0xe9ad, False, 'in Tower of Hera'), 'Tower of Hera - Map Chest': (0xe9ad, 0x186181, False, 'in Tower of Hera'),
'Tower of Hera - Big Key Chest': (0xe9e6, False, 'in Tower of Hera'), 'Tower of Hera - Big Key Chest': (0xe9e6, 0x1861ba, False, 'in Tower of Hera'),
'Tower of Hera - Compass Chest': (0xe9fb, False, 'in Tower of Hera'), 'Tower of Hera - Compass Chest': (0xe9fb, 0x1861cf, False, 'in Tower of Hera'),
'Tower of Hera - Big Chest': (0xe9f8, False, 'in Tower of Hera'), 'Tower of Hera - Big Chest': (0xe9f8, 0x1861cc, False, 'in Tower of Hera'),
'Tower of Hera - Boss': (0x180152, False, 'with Moldorm'), 'Tower of Hera - Boss': (0x180152, 0x186340, False, 'with Moldorm'),
'Pyramid': (0x180147, False, 'on the pyramid'), 'Pyramid': (0x180147, 0x186356, False, 'on the pyramid'),
'Catfish': (0xee185, False, 'with a catfish'), 'Catfish': (0xee185, 0x186361, False, 'with a catfish'),
'Stumpy': (0x330c7, False, 'with tree boy'), 'Stumpy': (0x330c7, 0x18636a, False, 'with tree boy'),
'Digging Game': (0x180148, False, 'underground'), 'Digging Game': (0x180148, 0x186357, False, 'underground'),
'Bombos Tablet': (0x180017, False, 'at a monolith'), 'Bombos Tablet': (0x180017, 0x18633c, False, 'at a monolith'),
'Hype Cave - Top': (0xeb1e, False, 'near a bat-like man'), 'Hype Cave - Top': (0xeb1e, 0x1862f2, False, 'near a bat-like man'),
'Hype Cave - Middle Right': (0xeb21, False, 'near a bat-like man'), 'Hype Cave - Middle Right': (0xeb21, 0x1862f5, False, 'near a bat-like man'),
'Hype Cave - Middle Left': (0xeb24, False, 'near a bat-like man'), 'Hype Cave - Middle Left': (0xeb24, 0x1862f8, False, 'near a bat-like man'),
'Hype Cave - Bottom': (0xeb27, False, 'near a bat-like man'), 'Hype Cave - Bottom': (0xeb27, 0x1862fb, False, 'near a bat-like man'),
'Hype Cave - Generous Guy': (0x180011, False, 'with a bat-like man'), 'Hype Cave - Generous Guy': (0x180011, 0x18635b, False, 'with a bat-like man'),
'Peg Cave': (0x180006, False, 'alone in a cave'), 'Peg Cave': (0x180006, 0x18634e, False, 'alone in a cave'),
'Pyramid Fairy - Left': (0xe980, False, 'near a fairy'), 'Pyramid Fairy - Left': (0xe980, 0x186154, False, 'near a fairy'),
'Pyramid Fairy - Right': (0xe983, False, 'near a fairy'), 'Pyramid Fairy - Right': (0xe983, 0x186157, False, 'near a fairy'),
'Brewery': (0xe9ec, False, 'alone in a home'), 'Brewery': (0xe9ec, 0x1861c0, False, 'alone in a home'),
'C-Shaped House': (0xe9ef, False, 'alone in a home'), 'C-Shaped House': (0xe9ef, 0x1861c3, False, 'alone in a home'),
'Chest Game': (0xeda8, False, 'as a prize'), 'Chest Game': (0xeda8, 0x18636b, False, 'as a prize'),
'Bumper Cave Ledge': (0x180146, False, 'on a ledge'), 'Bumper Cave Ledge': (0x180146, 0x186355, False, 'on a ledge'),
'Mire Shed - Left': (0xea73, False, 'near sparks'), 'Mire Shed - Left': (0xea73, 0x186247, False, 'near sparks'),
'Mire Shed - Right': (0xea76, False, 'near sparks'), 'Mire Shed - Right': (0xea76, 0x18624a, False, 'near sparks'),
'Superbunny Cave - Top': (0xea7c, False, 'in a connection'), 'Superbunny Cave - Top': (0xea7c, 0x186250, False, 'in a connection'),
'Superbunny Cave - Bottom': (0xea7f, False, 'in a connection'), 'Superbunny Cave - Bottom': (0xea7f, 0x186253, False, 'in a connection'),
'Spike Cave': (0xea8b, False, 'beyond spikes'), 'Spike Cave': (0xea8b, 0x18625f, False, 'beyond spikes'),
'Hookshot Cave - Top Right': (0xeb51, False, 'across pits'), 'Hookshot Cave - Top Right': (0xeb51, 0x186325, False, 'across pits'),
'Hookshot Cave - Top Left': (0xeb54, False, 'across pits'), 'Hookshot Cave - Top Left': (0xeb54, 0x186328, False, 'across pits'),
'Hookshot Cave - Bottom Right': (0xeb5a, False, 'across pits'), 'Hookshot Cave - Bottom Right': (0xeb5a, 0x18632e, False, 'across pits'),
'Hookshot Cave - Bottom Left': (0xeb57, False, 'across pits'), 'Hookshot Cave - Bottom Left': (0xeb57, 0x18632b, False, 'across pits'),
'Floating Island': (0x180141, False, 'on an island'), 'Floating Island': (0x180141, 0x186350, False, 'on an island'),
'Mimic Cave': (0xe9c5, False, 'in a cave of mimicry'), 'Mimic Cave': (0xe9c5, 0x186199, False, 'in a cave of mimicry'),
'Swamp Palace - Entrance': (0xea9d, False, 'in Swamp Palace'), 'Swamp Palace - Entrance': (0xea9d, 0x186271, False, 'in Swamp Palace'),
'Swamp Palace - Map Chest': (0xe986, False, 'in Swamp Palace'), 'Swamp Palace - Map Chest': (0xe986, 0x18615a, False, 'in Swamp Palace'),
'Swamp Palace - Big Chest': (0xe989, False, 'in Swamp Palace'), 'Swamp Palace - Big Chest': (0xe989, 0x18615d, False, 'in Swamp Palace'),
'Swamp Palace - Compass Chest': (0xeaa0, False, 'in Swamp Palace'), 'Swamp Palace - Compass Chest': (0xeaa0, 0x186274, False, 'in Swamp Palace'),
'Swamp Palace - Big Key Chest': (0xeaa6, False, 'in Swamp Palace'), 'Swamp Palace - Big Key Chest': (0xeaa6, 0x18627a, False, 'in Swamp Palace'),
'Swamp Palace - West Chest': (0xeaa3, False, 'in Swamp Palace'), 'Swamp Palace - West Chest': (0xeaa3, 0x186277, False, 'in Swamp Palace'),
'Swamp Palace - Flooded Room - Left': (0xeaa9, False, 'in Swamp Palace'), 'Swamp Palace - Flooded Room - Left': (0xeaa9, 0x18627d, False, 'in Swamp Palace'),
'Swamp Palace - Flooded Room - Right': (0xeaac, False, 'in Swamp Palace'), 'Swamp Palace - Flooded Room - Right': (0xeaac, 0x186280, False, 'in Swamp Palace'),
'Swamp Palace - Waterfall Room': (0xeaaf, False, 'in Swamp Palace'), 'Swamp Palace - Waterfall Room': (0xeaaf, 0x186283, False, 'in Swamp Palace'),
'Swamp Palace - Boss': (0x180154, False, 'with Arrghus'), 'Swamp Palace - Boss': (0x180154, 0x186342, False, 'with Arrghus'),
"Thieves' Town - Big Key Chest": (0xea04, False, "in Thieves' Town"), "Thieves' Town - Big Key Chest": (0xea04, 0x1861d8, False, "in Thieves' Town"),
"Thieves' Town - Map Chest": (0xea01, False, "in Thieves' Town"), "Thieves' Town - Map Chest": (0xea01, 0x1861d5, False, "in Thieves' Town"),
"Thieves' Town - Compass Chest": (0xea07, False, "in Thieves' Town"), "Thieves' Town - Compass Chest": (0xea07, 0x1861db, False, "in Thieves' Town"),
"Thieves' Town - Ambush Chest": (0xea0a, False, "in Thieves' Town"), "Thieves' Town - Ambush Chest": (0xea0a, 0x1861de, False, "in Thieves' Town"),
"Thieves' Town - Attic": (0xea0d, False, "in Thieves' Town"), "Thieves' Town - Attic": (0xea0d, 0x1861e1, False, "in Thieves' Town"),
"Thieves' Town - Big Chest": (0xea10, False, "in Thieves' Town"), "Thieves' Town - Big Chest": (0xea10, 0x1861e4, False, "in Thieves' Town"),
"Thieves' Town - Blind's Cell": (0xea13, False, "in Thieves' Town"), "Thieves' Town - Blind's Cell": (0xea13, 0x1861e7, False, "in Thieves' Town"),
"Thieves' Town - Boss": (0x180156, False, 'with Blind'), "Thieves' Town - Boss": (0x180156, 0x186344, False, 'with Blind'),
'Skull Woods - Compass Chest': (0xe992, False, 'in Skull Woods'), 'Skull Woods - Compass Chest': (0xe992, 0x186166, False, 'in Skull Woods'),
'Skull Woods - Map Chest': (0xe99b, False, 'in Skull Woods'), 'Skull Woods - Map Chest': (0xe99b, 0x18616f, False, 'in Skull Woods'),
'Skull Woods - Big Chest': (0xe998, False, 'in Skull Woods'), 'Skull Woods - Big Chest': (0xe998, 0x18616c, False, 'in Skull Woods'),
'Skull Woods - Pot Prison': (0xe9a1, False, 'in Skull Woods'), 'Skull Woods - Pot Prison': (0xe9a1, 0x186175, False, 'in Skull Woods'),
'Skull Woods - Pinball Room': (0xe9c8, False, 'in Skull Woods'), 'Skull Woods - Pinball Room': (0xe9c8, 0x18619c, False, 'in Skull Woods'),
'Skull Woods - Big Key Chest': (0xe99e, False, 'in Skull Woods'), 'Skull Woods - Big Key Chest': (0xe99e, 0x186172, False, 'in Skull Woods'),
'Skull Woods - Bridge Room': (0xe9fe, False, 'near Mothula'), 'Skull Woods - Bridge Room': (0xe9fe, 0x1861d2, False, 'near Mothula'),
'Skull Woods - Boss': (0x180155, False, 'with Mothula'), 'Skull Woods - Boss': (0x180155, 0x186343, False, 'with Mothula'),
'Ice Palace - Compass Chest': (0xe9d4, False, 'in Ice Palace'), 'Ice Palace - Compass Chest': (0xe9d4, 0x1861a8, False, 'in Ice Palace'),
'Ice Palace - Freezor Chest': (0xe995, False, 'in Ice Palace'), 'Ice Palace - Freezor Chest': (0xe995, 0x186169, False, 'in Ice Palace'),
'Ice Palace - Big Chest': (0xe9aa, False, 'in Ice Palace'), 'Ice Palace - Big Chest': (0xe9aa, 0x18617e, False, 'in Ice Palace'),
'Ice Palace - Iced T Room': (0xe9e3, False, 'in Ice Palace'), 'Ice Palace - Iced T Room': (0xe9e3, 0x1861b7, False, 'in Ice Palace'),
'Ice Palace - Spike Room': (0xe9e0, False, 'in Ice Palace'), 'Ice Palace - Spike Room': (0xe9e0, 0x1861b4, False, 'in Ice Palace'),
'Ice Palace - Big Key Chest': (0xe9a4, False, 'in Ice Palace'), 'Ice Palace - Big Key Chest': (0xe9a4, 0x186178, False, 'in Ice Palace'),
'Ice Palace - Map Chest': (0xe9dd, False, 'in Ice Palace'), 'Ice Palace - Map Chest': (0xe9dd, 0x1861b1, False, 'in Ice Palace'),
'Ice Palace - Boss': (0x180157, False, 'with Kholdstare'), 'Ice Palace - Boss': (0x180157, 0x186345, False, 'with Kholdstare'),
'Misery Mire - Big Chest': (0xea67, False, 'in Misery Mire'), 'Misery Mire - Big Chest': (0xea67, 0x18623b, False, 'in Misery Mire'),
'Misery Mire - Map Chest': (0xea6a, False, 'in Misery Mire'), 'Misery Mire - Map Chest': (0xea6a, 0x18623e, False, 'in Misery Mire'),
'Misery Mire - Main Lobby': (0xea5e, False, 'in Misery Mire'), 'Misery Mire - Main Lobby': (0xea5e, 0x186232, False, 'in Misery Mire'),
'Misery Mire - Bridge Chest': (0xea61, False, 'in Misery Mire'), 'Misery Mire - Bridge Chest': (0xea61, 0x186235, False, 'in Misery Mire'),
'Misery Mire - Spike Chest': (0xe9da, False, 'in Misery Mire'), 'Misery Mire - Spike Chest': (0xe9da, 0x1861ae, False, 'in Misery Mire'),
'Misery Mire - Compass Chest': (0xea64, False, 'in Misery Mire'), 'Misery Mire - Compass Chest': (0xea64, 0x186238, False, 'in Misery Mire'),
'Misery Mire - Big Key Chest': (0xea6d, False, 'in Misery Mire'), 'Misery Mire - Big Key Chest': (0xea6d, 0x186241, False, 'in Misery Mire'),
'Misery Mire - Boss': (0x180158, False, 'with Vitreous'), 'Misery Mire - Boss': (0x180158, 0x186346, False, 'with Vitreous'),
'Turtle Rock - Compass Chest': (0xea22, False, 'in Turtle Rock'), 'Turtle Rock - Compass Chest': (0xea22, 0x1861f6, False, 'in Turtle Rock'),
'Turtle Rock - Roller Room - Left': (0xea1c, False, 'in Turtle Rock'), 'Turtle Rock - Roller Room - Left': (0xea1c, 0x1861f0, False, 'in Turtle Rock'),
'Turtle Rock - Roller Room - Right': (0xea1f, False, 'in Turtle Rock'), 'Turtle Rock - Roller Room - Right': (0xea1f, 0x1861f3, False, 'in Turtle Rock'),
'Turtle Rock - Chain Chomps': (0xea16, False, 'in Turtle Rock'), 'Turtle Rock - Chain Chomps': (0xea16, 0x1861ea, False, 'in Turtle Rock'),
'Turtle Rock - Big Key Chest': (0xea25, False, 'in Turtle Rock'), 'Turtle Rock - Big Key Chest': (0xea25, 0x1861f9, False, 'in Turtle Rock'),
'Turtle Rock - Big Chest': (0xea19, False, 'in Turtle Rock'), 'Turtle Rock - Big Chest': (0xea19, 0x1861ed, False, 'in Turtle Rock'),
'Turtle Rock - Crystaroller Room': (0xea34, False, 'in Turtle Rock'), 'Turtle Rock - Crystaroller Room': (0xea34, 0x186208, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Bottom Left': (0xea31, False, 'in Turtle Rock'), 'Turtle Rock - Eye Bridge - Bottom Left': (0xea31, 0x186205, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Bottom Right': (0xea2e, False, 'in Turtle Rock'), 'Turtle Rock - Eye Bridge - Bottom Right': (0xea2e, 0x186202, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Top Left': (0xea2b, False, 'in Turtle Rock'), 'Turtle Rock - Eye Bridge - Top Left': (0xea2b, 0x1861ff, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Top Right': (0xea28, False, 'in Turtle Rock'), 'Turtle Rock - Eye Bridge - Top Right': (0xea28, 0x1861fc, False, 'in Turtle Rock'),
'Turtle Rock - Boss': (0x180159, False, 'with Trinexx'), 'Turtle Rock - Boss': (0x180159, 0x186347, False, 'with Trinexx'),
'Palace of Darkness - Shooter Room': (0xea5b, False, 'in Palace of Darkness'), 'Palace of Darkness - Shooter Room': (0xea5b, 0x18622f, False, 'in Palace of Darkness'),
'Palace of Darkness - The Arena - Bridge': (0xea3d, False, 'in Palace of Darkness'), 'Palace of Darkness - The Arena - Bridge': (0xea3d, 0x186211, False, 'in Palace of Darkness'),
'Palace of Darkness - Stalfos Basement': (0xea49, False, 'in Palace of Darkness'), 'Palace of Darkness - Stalfos Basement': (0xea49, 0x18621d, False, 'in Palace of Darkness'),
'Palace of Darkness - Big Key Chest': (0xea37, False, 'in Palace of Darkness'), 'Palace of Darkness - Big Key Chest': (0xea37, 0x18620b, False, 'in Palace of Darkness'),
'Palace of Darkness - The Arena - Ledge': (0xea3a, False, 'in Palace of Darkness'), 'Palace of Darkness - The Arena - Ledge': (0xea3a, 0x18620e, False, 'in Palace of Darkness'),
'Palace of Darkness - Map Chest': (0xea52, False, 'in Palace of Darkness'), 'Palace of Darkness - Map Chest': (0xea52, 0x186226, False, 'in Palace of Darkness'),
'Palace of Darkness - Compass Chest': (0xea43, False, 'in Palace of Darkness'), 'Palace of Darkness - Compass Chest': (0xea43, 0x186217, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Basement - Left': (0xea4c, False, 'in Palace of Darkness'), 'Palace of Darkness - Dark Basement - Left': (0xea4c, 0x186220, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Basement - Right': (0xea4f, False, 'in Palace of Darkness'), 'Palace of Darkness - Dark Basement - Right': (0xea4f, 0x186223, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Maze - Top': (0xea55, False, 'in Palace of Darkness'), 'Palace of Darkness - Dark Maze - Top': (0xea55, 0x186229, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Maze - Bottom': (0xea58, False, 'in Palace of Darkness'), 'Palace of Darkness - Dark Maze - Bottom': (0xea58, 0x18622c, False, 'in Palace of Darkness'),
'Palace of Darkness - Big Chest': (0xea40, False, 'in Palace of Darkness'), 'Palace of Darkness - Big Chest': (0xea40, 0x186214, False, 'in Palace of Darkness'),
'Palace of Darkness - Harmless Hellway': (0xea46, False, 'in Palace of Darkness'), 'Palace of Darkness - Harmless Hellway': (0xea46, 0x18621a, False, 'in Palace of Darkness'),
'Palace of Darkness - Boss': (0x180153, False, 'with Helmasaur King'), 'Palace of Darkness - Boss': (0x180153, 0x186341, False, 'with Helmasaur King'),
"Ganons Tower - Bob's Torch": (0x180161, False, "in Ganon's Tower"), "Ganons Tower - Bob's Torch": (0x180161, 0x186363, False, "in Ganon's Tower"),
'Ganons Tower - Hope Room - Left': (0xead9, False, "in Ganon's Tower"), 'Ganons Tower - Hope Room - Left': (0xead9, 0x1862ad, False, "in Ganon's Tower"),
'Ganons Tower - Hope Room - Right': (0xeadc, False, "in Ganon's Tower"), 'Ganons Tower - Hope Room - Right': (0xeadc, 0x1862b0, False, "in Ganon's Tower"),
'Ganons Tower - Tile Room': (0xeae2, False, "in Ganon's Tower"), 'Ganons Tower - Tile Room': (0xeae2, 0x1862b6, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Top Left': (0xeae5, False, "in Ganon's Tower"), 'Ganons Tower - Compass Room - Top Left': (0xeae5, 0x1862b9, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Top Right': (0xeae8, False, "in Ganon's Tower"), 'Ganons Tower - Compass Room - Top Right': (0xeae8, 0x1862bc, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Bottom Left': (0xeaeb, False, "in Ganon's Tower"), 'Ganons Tower - Compass Room - Bottom Left': (0xeaeb, 0x1862bf, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Bottom Right': (0xeaee, False, "in Ganon's Tower"), 'Ganons Tower - Compass Room - Bottom Right': (0xeaee, 0x1862c2, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Top Left': (0xeab8, False, "in Ganon's Tower"), 'Ganons Tower - DMs Room - Top Left': (0xeab8, 0x18628c, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Top Right': (0xeabb, False, "in Ganon's Tower"), 'Ganons Tower - DMs Room - Top Right': (0xeabb, 0x18628f, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Bottom Left': (0xeabe, False, "in Ganon's Tower"), 'Ganons Tower - DMs Room - Bottom Left': (0xeabe, 0x186292, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Bottom Right': (0xeac1, False, "in Ganon's Tower"), 'Ganons Tower - DMs Room - Bottom Right': (0xeac1, 0x186295, False, "in Ganon's Tower"),
'Ganons Tower - Map Chest': (0xead3, False, "in Ganon's Tower"), 'Ganons Tower - Map Chest': (0xead3, 0x1862a7, False, "in Ganon's Tower"),
'Ganons Tower - Firesnake Room': (0xead0, False, "in Ganon's Tower"), 'Ganons Tower - Firesnake Room': (0xead0, 0x1862a4, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Top Left': (0xeac4, False, "in Ganon's Tower"), 'Ganons Tower - Randomizer Room - Top Left': (0xeac4, 0x186298, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Top Right': (0xeac7, False, "in Ganon's Tower"), 'Ganons Tower - Randomizer Room - Top Right': (0xeac7, 0x18629b, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Bottom Left': (0xeaca, False, "in Ganon's Tower"), 'Ganons Tower - Randomizer Room - Bottom Left': (0xeaca, 0x18629e, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Bottom Right': (0xeacd, False, "in Ganon's Tower"), 'Ganons Tower - Randomizer Room - Bottom Right': (0xeacd, 0x1862a1, False, "in Ganon's Tower"),
"Ganons Tower - Bob's Chest": (0xeadf, False, "in Ganon's Tower"), "Ganons Tower - Bob's Chest": (0xeadf, 0x1862b3, False, "in Ganon's Tower"),
'Ganons Tower - Big Chest': (0xead6, False, "in Ganon's Tower"), 'Ganons Tower - Big Chest': (0xead6, 0x1862aa, False, "in Ganon's Tower"),
'Ganons Tower - Big Key Room - Left': (0xeaf4, False, "in Ganon's Tower"), 'Ganons Tower - Big Key Room - Left': (0xeaf4, 0x1862c8, False, "in Ganon's Tower"),
'Ganons Tower - Big Key Room - Right': (0xeaf7, False, "in Ganon's Tower"), 'Ganons Tower - Big Key Room - Right': (0xeaf7, 0x1862cb, False, "in Ganon's Tower"),
'Ganons Tower - Big Key Chest': (0xeaf1, False, "in Ganon's Tower"), 'Ganons Tower - Big Key Chest': (0xeaf1, 0x1862c5, False, "in Ganon's Tower"),
'Ganons Tower - Mini Helmasaur Room - Left': (0xeafd, False, "atop Ganon's Tower"), 'Ganons Tower - Mini Helmasaur Room - Left': (0xeafd, 0x1862d1, False, "atop Ganon's Tower"),
'Ganons Tower - Mini Helmasaur Room - Right': (0xeb00, False, "atop Ganon's Tower"), 'Ganons Tower - Mini Helmasaur Room - Right': (0xeb00, 0x1862d4, False, "atop Ganon's Tower"),
'Ganons Tower - Pre-Moldorm Chest': (0xeb03, False, "atop Ganon's Tower"), 'Ganons Tower - Pre-Moldorm Chest': (0xeb03, 0x1862d7, False, "atop Ganon's Tower"),
'Ganons Tower - Validation Chest': (0xeb06, False, "atop Ganon's Tower"), 'Ganons Tower - Validation Chest': (0xeb06, 0x1862da, False, "atop Ganon's Tower"),
'Ganon': (None, False, 'from me'), 'Ganon': (None, None, False, 'from me'),
'Agahnim 1': (None, False, 'from Ganon\'s wizardry form'), 'Agahnim 1': (None, None, False, 'from Ganon\'s wizardry form'),
'Agahnim 2': (None, False, 'from Ganon\'s wizardry form'), 'Agahnim 2': (None, None, False, 'from Ganon\'s wizardry form'),
'Floodgate': (None, False, None), 'Floodgate': (None, None, False, None),
'Frog': (None, False, None), 'Frog': (None, None, False, None),
'Missing Smith': (None, False, None), 'Missing Smith': (None, None, False, None),
'Dark Blacksmith Ruins': (None, False, None), 'Dark Blacksmith Ruins': (None, None, False, None),
'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], True, 'Eastern Palace'), 'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], None, True, 'Eastern Palace'),
'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], True, 'Desert Palace'), 'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], None, True, 'Desert Palace'),
'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], True, 'Tower of Hera'), 'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], None, True, 'Tower of Hera'),
'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], True, 'Palace of Darkness'), 'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], None, True, 'Palace of Darkness'),
'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], True, 'Swamp Palace'), 'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], None, True, 'Swamp Palace'),
'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], True, 'Thieves\' Town'), 'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], None, True, 'Thieves\' Town'),
'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], True, 'Skull Woods'), 'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], None, True, 'Skull Woods'),
'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], True, 'Ice Palace'), 'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], None, True, 'Ice Palace'),
'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], True, 'Misery Mire'), 'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], None, True, 'Misery Mire'),
'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], True, 'Turtle Rock')} 'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}

412
Rom.py
View File

@ -18,7 +18,7 @@ from EntranceShuffle import door_addresses
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '1907d4caccffe60fc69940cfa11b2dab' # RANDOMIZERBASEHASH = '1907d4caccffe60fc69940cfa11b2dab'
class JsonRom(object): class JsonRom(object):
@ -35,16 +35,6 @@ class JsonRom(object):
return return
self.patches[str(startaddress)] = list(values) self.patches[str(startaddress)] = list(values)
def write_int16(self, address, value):
self.write_bytes(address, int16_as_bytes(value))
def write_int16s(self, startaddress, values):
byte_list = [int16_as_bytes(value) for value in values]
self.patches[str(startaddress)] = [byte for bytes in byte_list for byte in bytes]
def write_int32(self, address, value):
self.write_bytes(address, int32_as_bytes(value))
def write_to_file(self, file): def write_to_file(self, file):
with open(file, 'w') as stream: with open(file, 'w') as stream:
json.dump([self.patches], stream) json.dump([self.patches], stream)
@ -54,8 +44,6 @@ class JsonRom(object):
h.update(json.dumps([self.patches]).encode('utf-8')) h.update(json.dumps([self.patches]).encode('utf-8'))
return h.hexdigest() return h.hexdigest()
class LocalRom(object): class LocalRom(object):
def __init__(self, file, patch=True): def __init__(self, file, patch=True):
@ -72,20 +60,6 @@ class LocalRom(object):
for i, value in enumerate(values): for i, value in enumerate(values):
self.write_byte(startaddress + i, value) self.write_byte(startaddress + i, value)
def write_int16(self, address, value):
self.write_bytes(address, int16_as_bytes(value))
def write_int16s(self, startaddress, values):
for i, value in enumerate(values):
self.write_int16(startaddress + (i * 2), value)
def write_int32(self, address, value):
self.write_bytes(address, int32_as_bytes(value))
def write_int32s(self, startaddress, values):
for i, value in enumerate(values):
self.write_int32(startaddress + (i * 2), value)
def write_to_file(self, file): def write_to_file(self, file):
with open(file, 'wb') as outfile: with open(file, 'wb') as outfile:
outfile.write(self.buffer) outfile.write(self.buffer)
@ -109,10 +83,10 @@ class LocalRom(object):
self.write_bytes(int(baseaddress), values) self.write_bytes(int(baseaddress), values)
# verify md5 # verify md5
patchedmd5 = hashlib.md5() # patchedmd5 = hashlib.md5()
patchedmd5.update(self.buffer) # patchedmd5.update(self.buffer)
if RANDOMIZERBASEHASH != patchedmd5.hexdigest(): # if RANDOMIZERBASEHASH != patchedmd5.hexdigest():
raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.') # raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
def patch_enemizer(self, rando_patch, base_enemizer_patch_path, enemizer_patch): def patch_enemizer(self, rando_patch, base_enemizer_patch_path, enemizer_patch):
# extend to 4MB # extend to 4MB
@ -142,6 +116,20 @@ class LocalRom(object):
h.update(self.buffer) h.update(self.buffer)
return h.hexdigest() return h.hexdigest()
def write_int16(rom, address, value):
rom.write_bytes(address, int16_as_bytes(value))
def write_int32(rom, address, value):
rom.write_bytes(address, int32_as_bytes(value))
def write_int16s(rom, startaddress, values):
for i, value in enumerate(values):
write_int16(rom, startaddress + (i * 2), value)
def write_int32s(rom, startaddress, values):
for i, value in enumerate(values):
write_int32(rom, startaddress + (i * 4), value)
def read_rom(stream): def read_rom(stream):
"Reads rom into bytearray and strips off any smc header" "Reads rom into bytearray and strips off any smc header"
buffer = bytearray(stream.read()) buffer = bytearray(stream.read())
@ -461,6 +449,11 @@ def patch_rom(world, player, rom):
itemid = 0x32 itemid = 0x32
if location.item.type == "SmallKey": if location.item.type == "SmallKey":
itemid = 0x24 itemid = 0x24
if location.item and location.item.player != player:
if location.player_address is not None:
rom.write_byte(location.player_address, location.item.player)
else:
itemid = 0x5A
rom.write_byte(location.address, itemid) rom.write_byte(location.address, itemid)
else: else:
# crystals # crystals
@ -490,9 +483,9 @@ def patch_rom(world, player, rom):
rom.write_byte(0x15B8C + offset, ow_area) rom.write_byte(0x15B8C + offset, ow_area)
rom.write_int16(0x15BDB + 2 * offset, vram_loc) write_int16(rom, 0x15BDB + 2 * offset, vram_loc)
rom.write_int16(0x15C79 + 2 * offset, scroll_y) write_int16(rom, 0x15C79 + 2 * offset, scroll_y)
rom.write_int16(0x15D17 + 2 * offset, scroll_x) write_int16(rom, 0x15D17 + 2 * offset, scroll_x)
# for positioning fixups we abuse the roomid as a way of identifying which exit data we are appling # for positioning fixups we abuse the roomid as a way of identifying which exit data we are appling
# Thanks to Zarby89 for originally finding these values # Thanks to Zarby89 for originally finding these values
@ -503,25 +496,25 @@ def patch_rom(world, player, rom):
'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit', 'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)', 'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit', 'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)',
'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)']: 'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)']:
# For exits that connot be reached from another, no need to apply offset fixes. # For exits that connot be reached from another, no need to apply offset fixes.
rom.write_int16(0x15DB5 + 2 * offset, link_y) # same as final else write_int16(rom, 0x15DB5 + 2 * offset, link_y) # same as final else
elif room_id == 0x0059 and world.fix_skullwoods_exit: elif room_id == 0x0059 and world.fix_skullwoods_exit:
rom.write_int16(0x15DB5 + 2 * offset, 0x00F8) write_int16(rom, 0x15DB5 + 2 * offset, 0x00F8)
elif room_id == 0x004a and world.fix_palaceofdarkness_exit: elif room_id == 0x004a and world.fix_palaceofdarkness_exit:
rom.write_int16(0x15DB5 + 2 * offset, 0x0640) write_int16(rom, 0x15DB5 + 2 * offset, 0x0640)
elif room_id == 0x00d6 and world.fix_trock_exit: elif room_id == 0x00d6 and world.fix_trock_exit:
rom.write_int16(0x15DB5 + 2 * offset, 0x0134) write_int16(rom, 0x15DB5 + 2 * offset, 0x0134)
elif room_id == 0x000c and world.fix_gtower_exit: # fix ganons tower exit point elif room_id == 0x000c and world.fix_gtower_exit: # fix ganons tower exit point
rom.write_int16(0x15DB5 + 2 * offset, 0x00A4) write_int16(rom, 0x15DB5 + 2 * offset, 0x00A4)
else: else:
rom.write_int16(0x15DB5 + 2 * offset, link_y) write_int16(rom, 0x15DB5 + 2 * offset, link_y)
rom.write_int16(0x15E53 + 2 * offset, link_x) write_int16(rom, 0x15E53 + 2 * offset, link_x)
rom.write_int16(0x15EF1 + 2 * offset, camera_y) write_int16(rom, 0x15EF1 + 2 * offset, camera_y)
rom.write_int16(0x15F8F + 2 * offset, camera_x) write_int16(rom, 0x15F8F + 2 * offset, camera_x)
rom.write_byte(0x1602D + offset, unknown_1) rom.write_byte(0x1602D + offset, unknown_1)
rom.write_byte(0x1607C + offset, unknown_2) rom.write_byte(0x1607C + offset, unknown_2)
rom.write_int16(0x160CB + 2 * offset, door_1) write_int16(rom, 0x160CB + 2 * offset, door_1)
rom.write_int16(0x16169 + 2 * offset, door_2) write_int16(rom, 0x16169 + 2 * offset, door_2)
elif isinstance(exit.addresses, list): elif isinstance(exit.addresses, list):
# is hole # is hole
for address in exit.addresses: for address in exit.addresses:
@ -605,7 +598,7 @@ def patch_rom(world, player, rom):
rom.write_byte(0x34FD6, 0x80) rom.write_byte(0x34FD6, 0x80)
overflow_replacement = GREEN_TWENTY_RUPEES overflow_replacement = GREEN_TWENTY_RUPEES
# Rupoor negative value # Rupoor negative value
rom.write_int16(0x180036, world.rupoor_cost) write_int16(rom, 0x180036, world.rupoor_cost)
# Set stun items # Set stun items
rom.write_byte(0x180180, 0x02) # Hookshot only rom.write_byte(0x180180, 0x02) # Hookshot only
elif world.difficulty_adjustments == 'expert': elif world.difficulty_adjustments == 'expert':
@ -623,7 +616,7 @@ def patch_rom(world, player, rom):
rom.write_byte(0x34FD6, 0x80) rom.write_byte(0x34FD6, 0x80)
overflow_replacement = GREEN_TWENTY_RUPEES overflow_replacement = GREEN_TWENTY_RUPEES
# Rupoor negative value # Rupoor negative value
rom.write_int16(0x180036, world.rupoor_cost) write_int16(rom, 0x180036, world.rupoor_cost)
# Set stun items # Set stun items
rom.write_byte(0x180180, 0x00) # Nothing rom.write_byte(0x180180, 0x00) # Nothing
else: else:
@ -640,7 +633,7 @@ def patch_rom(world, player, rom):
#Enable catching fairies #Enable catching fairies
rom.write_byte(0x34FD6, 0xF0) rom.write_byte(0x34FD6, 0xF0)
# Rupoor negative value # Rupoor negative value
rom.write_int16(0x180036, world.rupoor_cost) write_int16(rom, 0x180036, world.rupoor_cost)
# Set stun items # Set stun items
rom.write_byte(0x180180, 0x03) # All standard items rom.write_byte(0x180180, 0x03) # All standard items
#Set overflow items for progressive equipment #Set overflow items for progressive equipment
@ -658,15 +651,35 @@ def patch_rom(world, player, rom):
difficulty = world.difficulty_requirements difficulty = world.difficulty_requirements
#Set overflow items for progressive equipment #Set overflow items for progressive equipment
mw_sword_replacements = {0: overflow_replacement,
1: item_table['Fighter Sword'][3],
2: item_table['Master Sword'][3],
3: item_table['Tempered Sword'][3],
4: item_table['Golden Sword'][3]}
mw_shield_replacements = {0: overflow_replacement,
1: item_table['Blue Shield'][3],
2: item_table['Red Shield'][3],
3: item_table['Mirror Shield'][3]}
mw_armor_replacements = {0: overflow_replacement,
1: item_table['Blue Mail'][3],
2: item_table['Red Mail'][3]}
mw_bottle_replacements = {0: overflow_replacement,
1: item_table['Blue Potion'][3],
2: item_table['Blue Potion'][3],
3: item_table['Blue Potion'][3],
4: item_table['Blue Potion'][3]}
mw_bow_replacements = {0: overflow_replacement,
1: item_table['Bow'][3],
2: item_table['Bow'][3]}
rom.write_bytes(0x180090, rom.write_bytes(0x180090,
[difficulty.progressive_sword_limit, overflow_replacement, [difficulty.progressive_sword_limit, mw_sword_replacements[difficulty.progressive_sword_limit] if world.players > 1 else overflow_replacement,
difficulty.progressive_shield_limit, overflow_replacement, difficulty.progressive_shield_limit, mw_shield_replacements[difficulty.progressive_shield_limit] if world.players > 1 else overflow_replacement,
difficulty.progressive_armor_limit, overflow_replacement, difficulty.progressive_armor_limit, mw_armor_replacements[difficulty.progressive_armor_limit] if world.players > 1 else overflow_replacement,
difficulty.progressive_bottle_limit, overflow_replacement, difficulty.progressive_bottle_limit, mw_bottle_replacements[difficulty.progressive_bottle_limit] if world.players > 1 else overflow_replacement,
difficulty.progressive_bow_limit, overflow_replacement]) difficulty.progressive_bow_limit, mw_bow_replacements[difficulty.progressive_bow_limit] if world.players > 1 else overflow_replacement])
if difficulty.progressive_bow_limit < 2 and world.swords == 'swordless': if difficulty.progressive_bow_limit < 2 and world.swords == 'swordless':
rom.write_bytes(0x180098, [2, overflow_replacement]) rom.write_bytes(0x180098, [2, mw_bow_replacements[difficulty.progressive_bow_limit] if world.players > 1 else overflow_replacement])
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
# set up game internal RNG seed # set up game internal RNG seed
@ -802,37 +815,37 @@ def patch_rom(world, player, rom):
ERtimeincrease = ERtimeincrease + 15 ERtimeincrease = ERtimeincrease + 15
if world.clock_mode == 'off': if world.clock_mode == 'off':
rom.write_bytes(0x180190, [0x00, 0x00, 0x00]) # turn off clock mode rom.write_bytes(0x180190, [0x00, 0x00, 0x00]) # turn off clock mode
rom.write_int32(0x180200, 0) # red clock adjustment time (in frames, sint32) write_int32(rom, 0x180200, 0) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204, 0) # blue clock adjustment time (in frames, sint32) write_int32(rom, 0x180204, 0) # blue clock adjustment time (in frames, sint32)
rom.write_int32(0x180208, 0) # green clock adjustment time (in frames, sint32) write_int32(rom, 0x180208, 0) # green clock adjustment time (in frames, sint32)
rom.write_int32(0x18020C, 0) # starting time (in frames, sint32) write_int32(rom, 0x18020C, 0) # starting time (in frames, sint32)
elif world.clock_mode == 'ohko': elif world.clock_mode == 'ohko':
rom.write_bytes(0x180190, [0x01, 0x02, 0x01]) # ohko timer with resetable timer functionality rom.write_bytes(0x180190, [0x01, 0x02, 0x01]) # ohko timer with resetable timer functionality
rom.write_int32(0x180200, 0) # red clock adjustment time (in frames, sint32) write_int32(rom, 0x180200, 0) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204, 0) # blue clock adjustment time (in frames, sint32) write_int32(rom, 0x180204, 0) # blue clock adjustment time (in frames, sint32)
rom.write_int32(0x180208, 0) # green clock adjustment time (in frames, sint32) write_int32(rom, 0x180208, 0) # green clock adjustment time (in frames, sint32)
rom.write_int32(0x18020C, 0) # starting time (in frames, sint32) write_int32(rom, 0x18020C, 0) # starting time (in frames, sint32)
elif world.clock_mode == 'countdown-ohko': elif world.clock_mode == 'countdown-ohko':
rom.write_bytes(0x180190, [0x01, 0x02, 0x01]) # ohko timer with resetable timer functionality rom.write_bytes(0x180190, [0x01, 0x02, 0x01]) # ohko timer with resetable timer functionality
rom.write_int32(0x180200, -100 * 60 * 60 * 60) # red clock adjustment time (in frames, sint32) write_int32(rom, 0x180200, -100 * 60 * 60 * 60) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32) write_int32(rom, 0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32)
rom.write_int32(0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32) write_int32(rom, 0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32)
if world.difficulty_adjustments == 'normal': if world.difficulty_adjustments == 'normal':
rom.write_int32(0x18020C, (10 + ERtimeincrease) * 60 * 60) # starting time (in frames, sint32) write_int32(rom, 0x18020C, (10 + ERtimeincrease) * 60 * 60) # starting time (in frames, sint32)
else: else:
rom.write_int32(0x18020C, int((5 + ERtimeincrease / 2) * 60 * 60)) # starting time (in frames, sint32) write_int32(rom, 0x18020C, int((5 + ERtimeincrease / 2) * 60 * 60)) # starting time (in frames, sint32)
if world.clock_mode == 'stopwatch': if world.clock_mode == 'stopwatch':
rom.write_bytes(0x180190, [0x02, 0x01, 0x00]) # set stopwatch mode rom.write_bytes(0x180190, [0x02, 0x01, 0x00]) # set stopwatch mode
rom.write_int32(0x180200, -2 * 60 * 60) # red clock adjustment time (in frames, sint32) write_int32(rom, 0x180200, -2 * 60 * 60) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32) write_int32(rom, 0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32)
rom.write_int32(0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32) write_int32(rom, 0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32)
rom.write_int32(0x18020C, 0) # starting time (in frames, sint32) write_int32(rom, 0x18020C, 0) # starting time (in frames, sint32)
if world.clock_mode == 'countdown': if world.clock_mode == 'countdown':
rom.write_bytes(0x180190, [0x01, 0x01, 0x00]) # set countdown, with no reset available rom.write_bytes(0x180190, [0x01, 0x01, 0x00]) # set countdown, with no reset available
rom.write_int32(0x180200, -2 * 60 * 60) # red clock adjustment time (in frames, sint32) write_int32(rom, 0x180200, -2 * 60 * 60) # red clock adjustment time (in frames, sint32)
rom.write_int32(0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32) write_int32(rom, 0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32)
rom.write_int32(0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32) write_int32(rom, 0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32)
rom.write_int32(0x18020C, (40 + ERtimeincrease) * 60 * 60) # starting time (in frames, sint32) write_int32(rom, 0x18020C, (40 + ERtimeincrease) * 60 * 60) # starting time (in frames, sint32)
# set up goals for treasure hunt # set up goals for treasure hunt
rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon == 'Triforce Piece' else [0x0D, 0x28]) rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon == 'Triforce Piece' else [0x0D, 0x28])
@ -945,8 +958,8 @@ def patch_rom(world, player, rom):
return reveal_bytes.get(location.parent_region.dungeon.name, 0x0000) return reveal_bytes.get(location.parent_region.dungeon.name, 0x0000)
return 0x0000 return 0x0000
rom.write_int16(0x18017A, get_reveal_bytes('Green Pendant') if world.keysanity else 0x0000) # Sahasrahla reveal write_int16(rom, 0x18017A, get_reveal_bytes('Green Pendant') if world.keysanity else 0x0000) # Sahasrahla reveal
rom.write_int16(0x18017C, get_reveal_bytes('Crystal 5')|get_reveal_bytes('Crystal 6') if world.keysanity else 0x0000) # Bomb Shop Reveal write_int16(rom, 0x18017C, get_reveal_bytes('Crystal 5')|get_reveal_bytes('Crystal 6') if world.keysanity else 0x0000) # Bomb Shop Reveal
rom.write_byte(0x180172, 0x01 if world.retro else 0x00) # universal keys rom.write_byte(0x180172, 0x01 if world.retro else 0x00) # universal keys
rom.write_byte(0x180175, 0x01 if world.retro else 0x00) # rupee bow rom.write_byte(0x180175, 0x01 if world.retro else 0x00) # rupee bow
@ -974,14 +987,14 @@ def patch_rom(world, player, rom):
rom.write_bytes(0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) rom.write_bytes(0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
rom.write_byte(0x18004E, 0) # Escape Fill (nothing) rom.write_byte(0x18004E, 0) # Escape Fill (nothing)
rom.write_int16(0x180183, 300) # Escape fill rupee bow write_int16(rom, 0x180183, 300) # Escape fill rupee bow
rom.write_bytes(0x180185, [0,0,0]) # Uncle respawn refills (magic, bombs, arrows) rom.write_bytes(0x180185, [0,0,0]) # Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180188, [0,0,0]) # Zelda respawn refills (magic, bombs, arrows) rom.write_bytes(0x180188, [0,0,0]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0,0,0]) # Mantle respawn refills (magic, bombs, arrows) rom.write_bytes(0x18018B, [0,0,0]) # Mantle respawn refills (magic, bombs, arrows)
if world.mode == 'standard': if world.mode == 'standard':
if uncle_location.item is not None and uncle_location.item.name in ['Bow', 'Progressive Bow']: if uncle_location.item is not None and uncle_location.item.name in ['Bow', 'Progressive Bow']:
rom.write_byte(0x18004E, 1) # Escape Fill (arrows) rom.write_byte(0x18004E, 1) # Escape Fill (arrows)
rom.write_int16(0x180183, 300) # Escape fill rupee bow write_int16(rom, 0x180183, 300) # Escape fill rupee bow
rom.write_bytes(0x180185, [0,0,70]) # Uncle respawn refills (magic, bombs, arrows) rom.write_bytes(0x180185, [0,0,70]) # Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180188, [0,0,10]) # Zelda respawn refills (magic, bombs, arrows) rom.write_bytes(0x180188, [0,0,10]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0,0,10]) # Mantle respawn refills (magic, bombs, arrows) rom.write_bytes(0x18018B, [0,0,10]) # Mantle respawn refills (magic, bombs, arrows)
@ -1024,9 +1037,8 @@ def patch_rom(world, player, rom):
# set rom name # set rom name
# 21 bytes # 21 bytes
from Main import __version__ from Main import __version__
rom.name = bytearray('ER_{0}_{1:09}\0'.format(__version__[0:7],world.seed), 'utf8') rom.name = bytearray('ER{0}_{1}_{2:09}\0'.format(__version__.split('-')[0].replace('.','')[0:3], player, world.seed), 'utf8')
assert len(rom.name) <= 21 rom.write_bytes(0x7FC0, rom.name[0:21])
rom.write_bytes(0x7FC0, rom.name)
# Write title screen Code # Write title screen Code
hashint = int(rom.get_hash(), 16) hashint = int(rom.get_hash(), 16)
@ -1072,8 +1084,25 @@ def write_custom_shops(rom, world, player):
rom.write_bytes(0x184900, items_data) rom.write_bytes(0x184900, items_data)
def hud_format_text(text):
output = bytes()
for char in text.lower():
if 'a' <= char <= 'z':
output += bytes([0x5d + ord(char) - ord('a'), 0x29])
elif '0' <= char <= '8':
output += bytes([0x77 + ord(char) - ord('0'), 0x29])
elif char == '9':
output += b'\x4b\x29'
elif char == ' ':
output += b'\x7f\x00'
else:
output += b'\x2a\x29'
while len(output) < 32:
output += b'\x7f\x00'
return output[:32]
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite):
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, names = None):
# enable instant item menu # enable instant item menu
if fastmenu == 'instant': if fastmenu == 'instant':
@ -1132,6 +1161,11 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
if sprite is not None: if sprite is not None:
write_sprite(rom, sprite) write_sprite(rom, sprite)
# set player names
for player, name in names.items():
if 0 < player <= 64:
rom.write_bytes(0x185380 + ((player - 1) * 32), hud_format_text(name))
if isinstance(rom, LocalRom): if isinstance(rom, LocalRom):
rom.write_crc() rom.write_crc()
@ -1452,117 +1486,117 @@ def set_inverted_mode(world, rom):
rom.write_byte(snes_to_pc(0x05AF79), 0xF0) rom.write_byte(snes_to_pc(0x05AF79), 0xF0)
rom.write_byte(snes_to_pc(0x0DB3C5), 0xC6) rom.write_byte(snes_to_pc(0x0DB3C5), 0xC6)
rom.write_byte(snes_to_pc(0x07A3F4), 0xF0) # duck rom.write_byte(snes_to_pc(0x07A3F4), 0xF0) # duck
rom.write_int16s(snes_to_pc(0x02E849), [0x0043, 0x0056, 0x0058, 0x006C, 0x006F, 0x0070, 0x007B, 0x007F, 0x001B]) # dw flute write_int16s(rom, snes_to_pc(0x02E849), [0x0043, 0x0056, 0x0058, 0x006C, 0x006F, 0x0070, 0x007B, 0x007F, 0x001B]) # dw flute
rom.write_int16(snes_to_pc(0x02E8D5), 0x07C8) write_int16(rom, snes_to_pc(0x02E8D5), 0x07C8)
rom.write_int16(snes_to_pc(0x02E8F7), 0x01F8) write_int16(rom, snes_to_pc(0x02E8F7), 0x01F8)
rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof
# the following bytes should only be written in vanilla # the following bytes should only be written in vanilla
# or they'll overwrite the randomizer's shuffles # or they'll overwrite the randomizer's shuffles
if world.shuffle == 'vanilla': if world.shuffle == 'vanilla':
rom.write_byte(0xDBB73 + 0x23, 0x37) # switch AT and GT rom.write_byte(0xDBB73 + 0x23, 0x37) # switch AT and GT
rom.write_byte(0xDBB73 + 0x36, 0x24) rom.write_byte(0xDBB73 + 0x36, 0x24)
rom.write_int16(0x15AEE + 2*0x38, 0x00E0) write_int16(rom, 0x15AEE + 2*0x38, 0x00E0)
rom.write_int16(0x15AEE + 2*0x25, 0x000C) write_int16(rom, 0x15AEE + 2*0x25, 0x000C)
if world.shuffle in ['vanilla', 'dungeonssimple', 'dungeonsfull']: if world.shuffle in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
rom.write_byte(0x15B8C, 0x6C) rom.write_byte(0x15B8C, 0x6C)
rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house
rom.write_byte(0xDBB73 + 0x52, 0x01) rom.write_byte(0xDBB73 + 0x52, 0x01)
rom.write_byte(0xDBB73 + 0x15, 0x06) # bumper and old man cave rom.write_byte(0xDBB73 + 0x15, 0x06) # bumper and old man cave
rom.write_int16(0x15AEE + 2*0x17, 0x00F0) write_int16(rom, 0x15AEE + 2*0x17, 0x00F0)
rom.write_byte(0xDBB73 + 0x05, 0x16) rom.write_byte(0xDBB73 + 0x05, 0x16)
rom.write_int16(0x15AEE + 2*0x07, 0x00FB) write_int16(rom, 0x15AEE + 2*0x07, 0x00FB)
rom.write_byte(0xDBB73 + 0x2D, 0x17) rom.write_byte(0xDBB73 + 0x2D, 0x17)
rom.write_int16(0x15AEE + 2*0x2F, 0x00EB) write_int16(rom, 0x15AEE + 2*0x2F, 0x00EB)
rom.write_byte(0xDBB73 + 0x06, 0x2E) rom.write_byte(0xDBB73 + 0x06, 0x2E)
rom.write_int16(0x15AEE + 2*0x08, 0x00E6) write_int16(rom, 0x15AEE + 2*0x08, 0x00E6)
rom.write_byte(0xDBB73 + 0x16, 0x5E) rom.write_byte(0xDBB73 + 0x16, 0x5E)
rom.write_byte(0xDBB73 + 0x6F, 0x07) # DDM fairy to old man cave rom.write_byte(0xDBB73 + 0x6F, 0x07) # DDM fairy to old man cave
rom.write_int16(0x15AEE + 2*0x18, 0x00F1) write_int16(rom, 0x15AEE + 2*0x18, 0x00F1)
rom.write_byte(0x15B8C + 0x18, 0x43) rom.write_byte(0x15B8C + 0x18, 0x43)
rom.write_int16(0x15BDB + 2 * 0x18, 0x1400) write_int16(rom, 0x15BDB + 2 * 0x18, 0x1400)
rom.write_int16(0x15C79 + 2 * 0x18, 0x0294) write_int16(rom, 0x15C79 + 2 * 0x18, 0x0294)
rom.write_int16(0x15D17 + 2 * 0x18, 0x0600) write_int16(rom, 0x15D17 + 2 * 0x18, 0x0600)
rom.write_int16(0x15DB5 + 2 * 0x18, 0x02E8) write_int16(rom, 0x15DB5 + 2 * 0x18, 0x02E8)
rom.write_int16(0x15E53 + 2 * 0x18, 0x0678) write_int16(rom, 0x15E53 + 2 * 0x18, 0x0678)
rom.write_int16(0x15EF1 + 2 * 0x18, 0x0303) write_int16(rom, 0x15EF1 + 2 * 0x18, 0x0303)
rom.write_int16(0x15F8F + 2 * 0x18, 0x0685) write_int16(rom, 0x15F8F + 2 * 0x18, 0x0685)
rom.write_byte(0x1602D + 0x18, 0x0A) rom.write_byte(0x1602D + 0x18, 0x0A)
rom.write_byte(0x1607C + 0x18, 0xF6) rom.write_byte(0x1607C + 0x18, 0xF6)
rom.write_int16(0x160CB + 2 * 0x18, 0x0000) write_int16(rom, 0x160CB + 2 * 0x18, 0x0000)
rom.write_int16(0x16169 + 2 * 0x18, 0x0000) write_int16(rom, 0x16169 + 2 * 0x18, 0x0000)
rom.write_int16(0x15AEE + 2 * 0x3D, 0x0003) # pyramid exit and houlihan write_int16(rom, 0x15AEE + 2 * 0x3D, 0x0003) # pyramid exit and houlihan
rom.write_byte(0x15B8C + 0x3D, 0x5B) rom.write_byte(0x15B8C + 0x3D, 0x5B)
rom.write_int16(0x15BDB + 2 * 0x3D, 0x0B0E) write_int16(rom, 0x15BDB + 2 * 0x3D, 0x0B0E)
rom.write_int16(0x15C79 + 2 * 0x3D, 0x075A) write_int16(rom, 0x15C79 + 2 * 0x3D, 0x075A)
rom.write_int16(0x15D17 + 2 * 0x3D, 0x0674) write_int16(rom, 0x15D17 + 2 * 0x3D, 0x0674)
rom.write_int16(0x15DB5 + 2 * 0x3D, 0x07A8) write_int16(rom, 0x15DB5 + 2 * 0x3D, 0x07A8)
rom.write_int16(0x15E53 + 2 * 0x3D, 0x06E8) write_int16(rom, 0x15E53 + 2 * 0x3D, 0x06E8)
rom.write_int16(0x15EF1 + 2 * 0x3D, 0x07C7) write_int16(rom, 0x15EF1 + 2 * 0x3D, 0x07C7)
rom.write_int16(0x15F8F + 2 * 0x3D, 0x06F3) write_int16(rom, 0x15F8F + 2 * 0x3D, 0x06F3)
rom.write_byte(0x1602D + 0x3D, 0x06) rom.write_byte(0x1602D + 0x3D, 0x06)
rom.write_byte(0x1607C + 0x3D, 0xFA) rom.write_byte(0x1607C + 0x3D, 0xFA)
rom.write_int16(0x160CB + 2 * 0x3D, 0x0000) write_int16(rom, 0x160CB + 2 * 0x3D, 0x0000)
rom.write_int16(0x16169 + 2 * 0x3D, 0x0000) write_int16(rom, 0x16169 + 2 * 0x3D, 0x0000)
rom.write_int16(snes_to_pc(0x02D8D4), 0x112) # change sactuary spawn point to dark sanc write_int16(rom, snes_to_pc(0x02D8D4), 0x112) # change sactuary spawn point to dark sanc
rom.write_bytes(snes_to_pc(0x02D8E8), [0x22, 0x22, 0x22, 0x23, 0x04, 0x04, 0x04, 0x05]) rom.write_bytes(snes_to_pc(0x02D8E8), [0x22, 0x22, 0x22, 0x23, 0x04, 0x04, 0x04, 0x05])
rom.write_int16(snes_to_pc(0x02D91A), 0x0400) write_int16(rom, snes_to_pc(0x02D91A), 0x0400)
rom.write_int16(snes_to_pc(0x02D928), 0x222E) write_int16(rom, snes_to_pc(0x02D928), 0x222E)
rom.write_int16(snes_to_pc(0x02D936), 0x229A) write_int16(rom, snes_to_pc(0x02D936), 0x229A)
rom.write_int16(snes_to_pc(0x02D944), 0x0480) write_int16(rom, snes_to_pc(0x02D944), 0x0480)
rom.write_int16(snes_to_pc(0x02D952), 0x00A5) write_int16(rom, snes_to_pc(0x02D952), 0x00A5)
rom.write_int16(snes_to_pc(0x02D960), 0x007F) write_int16(rom, snes_to_pc(0x02D960), 0x007F)
rom.write_byte(snes_to_pc(0x02D96D), 0x14) rom.write_byte(snes_to_pc(0x02D96D), 0x14)
rom.write_byte(snes_to_pc(0x02D974), 0x00) rom.write_byte(snes_to_pc(0x02D974), 0x00)
rom.write_byte(snes_to_pc(0x02D97B), 0xFF) rom.write_byte(snes_to_pc(0x02D97B), 0xFF)
rom.write_byte(snes_to_pc(0x02D982), 0x00) rom.write_byte(snes_to_pc(0x02D982), 0x00)
rom.write_byte(snes_to_pc(0x02D989), 0x02) rom.write_byte(snes_to_pc(0x02D989), 0x02)
rom.write_byte(snes_to_pc(0x02D990), 0x00) rom.write_byte(snes_to_pc(0x02D990), 0x00)
rom.write_int16(snes_to_pc(0x02D998), 0x0000) write_int16(rom, snes_to_pc(0x02D998), 0x0000)
rom.write_int16(snes_to_pc(0x02D9A6), 0x005A) write_int16(rom, snes_to_pc(0x02D9A6), 0x005A)
rom.write_byte(snes_to_pc(0x02D9B3), 0x12) rom.write_byte(snes_to_pc(0x02D9B3), 0x12)
# keep the old man spawn point at old man house unless shuffle is vanilla # keep the old man spawn point at old man house unless shuffle is vanilla
if world.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple']: if world.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple']:
rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01]) rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01])
rom.write_int16(snes_to_pc(0x02D8DE), 0x00F1) write_int16(rom, snes_to_pc(0x02D8DE), 0x00F1)
rom.write_bytes(snes_to_pc(0x02D910), [0x1F, 0x1E, 0x1F, 0x1F, 0x03, 0x02, 0x03, 0x03]) rom.write_bytes(snes_to_pc(0x02D910), [0x1F, 0x1E, 0x1F, 0x1F, 0x03, 0x02, 0x03, 0x03])
rom.write_int16(snes_to_pc(0x02D924), 0x0300) write_int16(rom, snes_to_pc(0x02D924), 0x0300)
rom.write_int16(snes_to_pc(0x02D932), 0x1F10) write_int16(rom, snes_to_pc(0x02D932), 0x1F10)
rom.write_int16(snes_to_pc(0x02D940), 0x1FC0) write_int16(rom, snes_to_pc(0x02D940), 0x1FC0)
rom.write_int16(snes_to_pc(0x02D94E), 0x0378) write_int16(rom, snes_to_pc(0x02D94E), 0x0378)
rom.write_int16(snes_to_pc(0x02D95C), 0x0187) write_int16(rom, snes_to_pc(0x02D95C), 0x0187)
rom.write_int16(snes_to_pc(0x02D96A), 0x017F) write_int16(rom, snes_to_pc(0x02D96A), 0x017F)
rom.write_byte(snes_to_pc(0x02D972), 0x06) rom.write_byte(snes_to_pc(0x02D972), 0x06)
rom.write_byte(snes_to_pc(0x02D979), 0x00) rom.write_byte(snes_to_pc(0x02D979), 0x00)
rom.write_byte(snes_to_pc(0x02D980), 0xFF) rom.write_byte(snes_to_pc(0x02D980), 0xFF)
rom.write_byte(snes_to_pc(0x02D987), 0x00) rom.write_byte(snes_to_pc(0x02D987), 0x00)
rom.write_byte(snes_to_pc(0x02D98E), 0x22) rom.write_byte(snes_to_pc(0x02D98E), 0x22)
rom.write_byte(snes_to_pc(0x02D995), 0x12) rom.write_byte(snes_to_pc(0x02D995), 0x12)
rom.write_int16(snes_to_pc(0x02D9A2), 0x0000) write_int16(rom, snes_to_pc(0x02D9A2), 0x0000)
rom.write_int16(snes_to_pc(0x02D9B0), 0x0007) write_int16(rom, snes_to_pc(0x02D9B0), 0x0007)
rom.write_byte(snes_to_pc(0x02D9B8), 0x12) rom.write_byte(snes_to_pc(0x02D9B8), 0x12)
rom.write_bytes(0x180247, [0x00, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00]) rom.write_bytes(0x180247, [0x00, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00])
rom.write_int16(0x15AEE + 2 * 0x06, 0x0020) # post aga hyrule castle spawn write_int16(rom, 0x15AEE + 2 * 0x06, 0x0020) # post aga hyrule castle spawn
rom.write_byte(0x15B8C + 0x06, 0x1B) rom.write_byte(0x15B8C + 0x06, 0x1B)
rom.write_int16(0x15BDB + 2 * 0x06, 0x00AE) write_int16(rom, 0x15BDB + 2 * 0x06, 0x00AE)
rom.write_int16(0x15C79 + 2 * 0x06, 0x0610) write_int16(rom, 0x15C79 + 2 * 0x06, 0x0610)
rom.write_int16(0x15D17 + 2 * 0x06, 0x077E) write_int16(rom, 0x15D17 + 2 * 0x06, 0x077E)
rom.write_int16(0x15DB5 + 2 * 0x06, 0x0672) write_int16(rom, 0x15DB5 + 2 * 0x06, 0x0672)
rom.write_int16(0x15E53 + 2 * 0x06, 0x07F8) write_int16(rom, 0x15E53 + 2 * 0x06, 0x07F8)
rom.write_int16(0x15EF1 + 2 * 0x06, 0x067D) write_int16(rom, 0x15EF1 + 2 * 0x06, 0x067D)
rom.write_int16(0x15F8F + 2 * 0x06, 0x0803) write_int16(rom, 0x15F8F + 2 * 0x06, 0x0803)
rom.write_byte(0x1602D + 0x06, 0x00) rom.write_byte(0x1602D + 0x06, 0x00)
rom.write_byte(0x1607C + 0x06, 0xF2) rom.write_byte(0x1607C + 0x06, 0xF2)
rom.write_int16(0x160CB + 2 * 0x06, 0x0000) write_int16(rom, 0x160CB + 2 * 0x06, 0x0000)
rom.write_int16(0x16169 + 2 * 0x06, 0x0000) write_int16(rom, 0x16169 + 2 * 0x06, 0x0000)
rom.write_int16(snes_to_pc(0x02E87B), 0x00AE) # move flute splot 9 write_int16(rom, snes_to_pc(0x02E87B), 0x00AE) # move flute splot 9
rom.write_int16(snes_to_pc(0x02E89D), 0x0610) write_int16(rom, snes_to_pc(0x02E89D), 0x0610)
rom.write_int16(snes_to_pc(0x02E8BF), 0x077E) write_int16(rom, snes_to_pc(0x02E8BF), 0x077E)
rom.write_int16(snes_to_pc(0x02E8E1), 0x0672) write_int16(rom, snes_to_pc(0x02E8E1), 0x0672)
rom.write_int16(snes_to_pc(0x02E903), 0x07F8) write_int16(rom, snes_to_pc(0x02E903), 0x07F8)
rom.write_int16(snes_to_pc(0x02E925), 0x067D) write_int16(rom, snes_to_pc(0x02E925), 0x067D)
rom.write_int16(snes_to_pc(0x02E947), 0x0803) write_int16(rom, snes_to_pc(0x02E947), 0x0803)
rom.write_int16(snes_to_pc(0x02E969), 0x0000) write_int16(rom, snes_to_pc(0x02E969), 0x0000)
rom.write_int16(snes_to_pc(0x02E98B), 0xFFF2) write_int16(rom, snes_to_pc(0x02E98B), 0xFFF2)
rom.write_byte(snes_to_pc(0x1AF696), 0xF0) # bat sprite retreat rom.write_byte(snes_to_pc(0x1AF696), 0xF0) # bat sprite retreat
rom.write_byte(snes_to_pc(0x1AF6B2), 0x33) rom.write_byte(snes_to_pc(0x1AF6B2), 0x33)
rom.write_bytes(snes_to_pc(0x1AF730), [0x6A, 0x9E, 0x0C, 0x00, 0x7A, 0x9E, 0x0C, rom.write_bytes(snes_to_pc(0x1AF730), [0x6A, 0x9E, 0x0C, 0x00, 0x7A, 0x9E, 0x0C,
@ -1570,7 +1604,7 @@ def set_inverted_mode(world, rom):
0x0C, 0x00, 0x7A, 0xAE, 0x0C, 0x00, 0x8A, 0x0C, 0x00, 0x7A, 0xAE, 0x0C, 0x00, 0x8A,
0xAE, 0x0C, 0x00, 0x67, 0x97, 0x0C, 0x00, 0xAE, 0x0C, 0x00, 0x67, 0x97, 0x0C, 0x00,
0x8D, 0x97, 0x0C, 0x00]) 0x8D, 0x97, 0x0C, 0x00])
rom.write_int16s(snes_to_pc(0x0FF1C8), [0x190F, 0x190F, 0x190F, 0x194C, 0x190F, write_int16s(rom, snes_to_pc(0x0FF1C8), [0x190F, 0x190F, 0x190F, 0x194C, 0x190F,
0x194B, 0x190F, 0x195C, 0x594B, 0x194C, 0x194B, 0x190F, 0x195C, 0x594B, 0x194C,
0x19EE, 0x19EE, 0x194B, 0x19EE, 0x19EE, 0x19EE, 0x19EE, 0x194B, 0x19EE, 0x19EE,
0x19EE, 0x594B, 0x190F, 0x595C, 0x190F, 0x19EE, 0x594B, 0x190F, 0x595C, 0x190F,
@ -1578,38 +1612,38 @@ def set_inverted_mode(world, rom):
0x19EE, 0x195C, 0x19EE, 0x19EE, 0x19EE, 0x19EE, 0x195C, 0x19EE, 0x19EE, 0x19EE,
0x19EE, 0x595C, 0x595B, 0x190F, 0x190F, 0x19EE, 0x595C, 0x595B, 0x190F, 0x190F,
0x190F]) 0x190F])
rom.write_int16s(snes_to_pc(0x0FA480), [0x190F, 0x196B, 0x9D04, 0x9D04, 0x196B, write_int16s(rom, snes_to_pc(0x0FA480), [0x190F, 0x196B, 0x9D04, 0x9D04, 0x196B,
0x190F, 0x9D04, 0x9D04]) 0x190F, 0x9D04, 0x9D04])
rom.write_int16s(snes_to_pc(0x1bb810), [0x00BE, 0x00C0, 0x013E]) write_int16s(rom, snes_to_pc(0x1bb810), [0x00BE, 0x00C0, 0x013E])
rom.write_int16s(snes_to_pc(0x1bb836), [0x001B, 0x001B, 0x001B]) write_int16s(rom, snes_to_pc(0x1bb836), [0x001B, 0x001B, 0x001B])
rom.write_int16(snes_to_pc(0x308300), 0x0140) # new pyramid hole entrance write_int16(rom, snes_to_pc(0x308300), 0x0140) # new pyramid hole entrance
rom.write_int16(snes_to_pc(0x308320), 0x001B) write_int16(rom, snes_to_pc(0x308320), 0x001B)
if world.shuffle in ['vanilla', 'dungeonssimple', 'dungeonsfull']: if world.shuffle in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
rom.write_byte(snes_to_pc(0x308340), 0x7B) rom.write_byte(snes_to_pc(0x308340), 0x7B)
rom.write_int16(snes_to_pc(0x1af504), 0x148B) write_int16(rom, snes_to_pc(0x1af504), 0x148B)
rom.write_int16(snes_to_pc(0x1af50c), 0x149B) write_int16(rom, snes_to_pc(0x1af50c), 0x149B)
rom.write_int16(snes_to_pc(0x1af514), 0x14A4) write_int16(rom, snes_to_pc(0x1af514), 0x14A4)
rom.write_int16(snes_to_pc(0x1af51c), 0x1489) write_int16(rom, snes_to_pc(0x1af51c), 0x1489)
rom.write_int16(snes_to_pc(0x1af524), 0x14AC) write_int16(rom, snes_to_pc(0x1af524), 0x14AC)
rom.write_int16(snes_to_pc(0x1af52c), 0x54AC) write_int16(rom, snes_to_pc(0x1af52c), 0x54AC)
rom.write_int16(snes_to_pc(0x1af534), 0x148C) write_int16(rom, snes_to_pc(0x1af534), 0x148C)
rom.write_int16(snes_to_pc(0x1af53c), 0x548C) write_int16(rom, snes_to_pc(0x1af53c), 0x548C)
rom.write_int16(snes_to_pc(0x1af544), 0x1484) write_int16(rom, snes_to_pc(0x1af544), 0x1484)
rom.write_int16(snes_to_pc(0x1af54c), 0x5484) write_int16(rom, snes_to_pc(0x1af54c), 0x5484)
rom.write_int16(snes_to_pc(0x1af554), 0x14A2) write_int16(rom, snes_to_pc(0x1af554), 0x14A2)
rom.write_int16(snes_to_pc(0x1af55c), 0x54A2) write_int16(rom, snes_to_pc(0x1af55c), 0x54A2)
rom.write_int16(snes_to_pc(0x1af564), 0x14A0) write_int16(rom, snes_to_pc(0x1af564), 0x14A0)
rom.write_int16(snes_to_pc(0x1af56c), 0x54A0) write_int16(rom, snes_to_pc(0x1af56c), 0x54A0)
rom.write_int16(snes_to_pc(0x1af574), 0x148E) write_int16(rom, snes_to_pc(0x1af574), 0x148E)
rom.write_int16(snes_to_pc(0x1af57c), 0x548E) write_int16(rom, snes_to_pc(0x1af57c), 0x548E)
rom.write_int16(snes_to_pc(0x1af584), 0x14AE) write_int16(rom, snes_to_pc(0x1af584), 0x14AE)
rom.write_int16(snes_to_pc(0x1af58c), 0x54AE) write_int16(rom, snes_to_pc(0x1af58c), 0x54AE)
rom.write_byte(snes_to_pc(0x00DB9D), 0x1A) # castle hole graphics rom.write_byte(snes_to_pc(0x00DB9D), 0x1A) # castle hole graphics
rom.write_byte(snes_to_pc(0x00DC09), 0x1A) rom.write_byte(snes_to_pc(0x00DC09), 0x1A)
rom.write_byte(snes_to_pc(0x00D009), 0x31) rom.write_byte(snes_to_pc(0x00D009), 0x31)
rom.write_byte(snes_to_pc(0x00D0e8), 0xE0) rom.write_byte(snes_to_pc(0x00D0e8), 0xE0)
rom.write_byte(snes_to_pc(0x00D1c7), 0x00) rom.write_byte(snes_to_pc(0x00D1c7), 0x00)
rom.write_int16(snes_to_pc(0x1BE8DA), 0x39AD) write_int16(rom, snes_to_pc(0x1BE8DA), 0x39AD)
rom.write_byte(0xF6E58, 0x80) # no whirlpool under castle gate rom.write_byte(0xF6E58, 0x80) # no whirlpool under castle gate
rom.write_bytes(0x0086E, [0x5C, 0x00, 0xA0, 0xA1]) # TR tail rom.write_bytes(0x0086E, [0x5C, 0x00, 0xA0, 0xA1]) # TR tail
rom.write_bytes(snes_to_pc(0x1BC67A), [0x2E, 0x0B, 0x82]) # add warps under rocks rom.write_bytes(snes_to_pc(0x1BC67A), [0x2E, 0x0B, 0x82]) # add warps under rocks
@ -1619,25 +1653,25 @@ def set_inverted_mode(world, rom):
rom.write_bytes(snes_to_pc(0x1BC3DF), [0xD8, 0xD1]) rom.write_bytes(snes_to_pc(0x1BC3DF), [0xD8, 0xD1])
rom.write_bytes(snes_to_pc(0x1BD1D8), [0xA8, 0x02, 0x82, 0xFF, 0xFF]) rom.write_bytes(snes_to_pc(0x1BD1D8), [0xA8, 0x02, 0x82, 0xFF, 0xFF])
rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82]) rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82])
rom.write_int16(0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door write_int16(rom, 0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door
rom.write_int16(0xDBA71 + 2 * 0x35, 0x06A4) write_int16(rom, 0xDBA71 + 2 * 0x35, 0x06A4)
if world.shuffle in ['vanilla', 'dungeonssimple', 'dungeonsfull']: if world.shuffle in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
rom.write_byte(0xDBB73 + 0x35, 0x36) rom.write_byte(0xDBB73 + 0x35, 0x36)
rom.write_byte(snes_to_pc(0x09D436), 0xF3) # remove castle gate warp rom.write_byte(snes_to_pc(0x09D436), 0xF3) # remove castle gate warp
if world.shuffle in ['vanilla', 'dungeonssimple', 'dungeonsfull']: if world.shuffle in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
rom.write_int16(0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area write_int16(rom, 0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area
rom.write_byte(0x15B8C + 0x37, 0x1B) rom.write_byte(0x15B8C + 0x37, 0x1B)
rom.write_int16(0x15BDB + 2 * 0x37, 0x0418) write_int16(rom, 0x15BDB + 2 * 0x37, 0x0418)
rom.write_int16(0x15C79 + 2 * 0x37, 0x0679) write_int16(rom, 0x15C79 + 2 * 0x37, 0x0679)
rom.write_int16(0x15D17 + 2 * 0x37, 0x06B4) write_int16(rom, 0x15D17 + 2 * 0x37, 0x06B4)
rom.write_int16(0x15DB5 + 2 * 0x37, 0x06C6) write_int16(rom, 0x15DB5 + 2 * 0x37, 0x06C6)
rom.write_int16(0x15E53 + 2 * 0x37, 0x0738) write_int16(rom, 0x15E53 + 2 * 0x37, 0x0738)
rom.write_int16(0x15EF1 + 2 * 0x37, 0x06E6) write_int16(rom, 0x15EF1 + 2 * 0x37, 0x06E6)
rom.write_int16(0x15F8F + 2 * 0x37, 0x0733) write_int16(rom, 0x15F8F + 2 * 0x37, 0x0733)
rom.write_byte(0x1602D + 0x37, 0x07) rom.write_byte(0x1602D + 0x37, 0x07)
rom.write_byte(0x1607C + 0x37, 0xF9) rom.write_byte(0x1607C + 0x37, 0xF9)
rom.write_int16(0x160CB + 2 * 0x37, 0x0000) write_int16(rom, 0x160CB + 2 * 0x37, 0x0000)
rom.write_int16(0x16169 + 2 * 0x37, 0x0000) write_int16(rom, 0x16169 + 2 * 0x37, 0x0000)
rom.write_bytes(snes_to_pc(0x1BC387), [0xDD, 0xD1]) rom.write_bytes(snes_to_pc(0x1BC387), [0xDD, 0xD1])
rom.write_bytes(snes_to_pc(0x1BD1DD), [0xA4, 0x06, 0x82, 0x9E, 0x06, 0x82, 0xFF, 0xFF]) rom.write_bytes(snes_to_pc(0x1BD1DD), [0xA4, 0x06, 0x82, 0x9E, 0x06, 0x82, 0xFF, 0xFF])
rom.write_byte(0x180089, 0x01) # open TR after exit rom.write_byte(0x180089, 0x01) # open TR after exit
@ -1652,9 +1686,9 @@ def patch_shuffled_dark_sanc(world, rom, player):
rom.write_byte(0x180241, 0x01) rom.write_byte(0x180241, 0x01)
rom.write_byte(0x180248, door_index + 1) rom.write_byte(0x180248, door_index + 1)
rom.write_int16(0x180250, room_id) write_int16(rom, 0x180250, room_id)
rom.write_byte(0x180252, ow_area) rom.write_byte(0x180252, ow_area)
rom.write_int16s(0x180253, [vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x]) write_int16s(rom, 0x180253, [vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x])
rom.write_bytes(0x180262, [unknown_1, unknown_2, 0x00]) rom.write_bytes(0x180262, [unknown_1, unknown_2, 0x00])
InconvenientDungeonEntrances = {'Turtle Rock': 'Turtle Rock Main', InconvenientDungeonEntrances = {'Turtle Rock': 'Turtle Rock Main',

View File

@ -1,7 +1,11 @@
import os import os
import re
import subprocess import subprocess
import sys import sys
def parse_names_string(names):
return {player: name for player, name in enumerate([n for n in re.split(r'[, ]', names) if n], 1)}
def int16_as_bytes(value): def int16_as_bytes(value):
value = value & 0xFFFF value = value & 0xFFFF
return [value & 0xFF, (value >> 8) & 0xFF] return [value & 0xFF, (value >> 8) & 0xFF]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long