Archipelago/worlds/oot/SaveContext.py

1001 lines
43 KiB
Python
Raw Normal View History

Ocarina of Time (#64) * first commit (not including OoT data files yet) * added some basic options * rule parser works now at least * make sure to commit everything this time * temporary change to BaseClasses for oot * overworld location graph builds mostly correctly * adding oot data files * commenting out world options until later since they only existed to make the RuleParser work * conversion functions between AP ids and OOT ids * world graph outputs * set scrub prices * itempool generates, entrances connected, way too many options added * fixed set_rules and set_shop_rules * temp baseclasses changes * Reaches the fill step now, old event-based system retained in case the new way breaks * Song placements and misc fixes everywhere * temporary changes to make oot work * changed root exits for AP fill framework * prevent infinite recursion due to OoT sharing usage of the address field * age reachability works hopefully, songs are broken again * working spoiler log generation on beatable-only * Logic tricks implemented * need this for logic tricks * fixed map/compass being placed on Serenade location * kill unreachable events before filling the world * add a bunch of utility functions to prepare for rom patching * move OptionList into generic options * fixed some silly bugs with OptionList * properly seed all random behavior (so far) * ROM generation working * fix hints trying to get alttp dungeon hint texts * continue fixing hints * add oot to network data package * change item and location IDs to 66000 and 67000 range respectively * push removed items to precollected items * fixed various issues with cross-contamination with multiple world generation * reenable glitched logic (hopefully) * glitched world files age-check fix * cleaned up some get_locations calls * added token shuffle and scrub shuffle, modified some options slightly to make the parsing work * reenable MQ dungeons * fix forest mq exception * made targeting style an option for now, will be cosmetic later * reminder to move targeting to cosmetics * some oot option maintenance * enabled starting time of day * fixed issue breaking shop slots in multiworld generation * added "off" option for text shuffle and hints * shopsanity functionality restored * change patch file extension * remove unnecessary utility functions + imports * update MIT license * change option to "patch_uncompressed_rom" instead of "compress_rom" * compliance with new AutoWorld systems * Kill only internal events, remove non-internal big poe event in code * re-add the big poe event and handle it correctly * remove extra method in Range option * fix typo * Starting items, starting with consumables option * do not remove nonexistent item * move set_shop_rules to after shop items are placed * some cleanup * add retries for song placement * flagged Skull Mask and Mask of Truth as advancement items * update OoT to use LogicMixin * Fixed trying to assign starting items from the wrong players * fixed song retry step * improved option handling, comments, and starting item replacements * DefaultOnToggle writes Yes or No to spoiler * enable compression of output if Compress executable is present * clean up compression * check whether (de)compressor exists before running the process * allow specification of rom path in host.yaml * check if decompressed file already exists before decompressing again * fix triforce hunt generation * rename all the oot state functions with prefix * OoT: mark triforce pieces as completion goal for triforce hunt * added overworld and any-dungeon shuffle for dungeon items * Hide most unshuffled locations and events from the list of locations in spoiler * build oot option ranges with a generic function instead of defining each separately * move oot output-type control to host.yaml instead of individual yamls * implement dungeon song shuffle * minor improvements to overworld dungeon item shuffle * remove random ice trap names in shops, mostly to avoid maintaining a massive censor list * always output patch file to folder, remove option to generate ROM in preparation for removal * re-add the fix for infinite recursion due to not being light or dark world * change AP-sendable to Ocarina of Time model, since the triforce piece has some extra code apparently * oot: remove item_names and location_names * oot: minor fixes * oot: comment out ROM patching * oot: only add CollectionState objects on creation if actually needed * main entrance shuffle method and entrances-based rules * fix entrances based rules * disable master quest and big poe count options for client compatibility * use get_player_name instead of get_player_names * fix OptionList * fix oot options for new option system * new coop section in oot rom: expand player names to 16 bytes, write AP_PLAYER_NAME at end of PLAYER_NAMES * fill AP player name in oot rom with 0 instead of 0xDF * encode player name with ASCII for fixed-width * revert oot player name array to 8 bytes per name * remove Pierre location if fast scarecrow is on * check player name length * "free_scarecrow" not "fast_scarecrow" * OoT locations now properly store the AP ID instead of the oot internal ID * oot __version__ updates in lockstep with AP version * pull in unmodified oot cosmetic files * also grab JSONDump since it's needed apparently * gather extra needed methods, modify imports * delete cosmetics log, replace all instances of SettingsList with OOTWorld * cosmetic options working, except for sound effects (due to ear-safe issues) * SFX, Music, and Fanfare randomization reenabled * move OoT data files into the worlds folder * move Compress and Decompress into oot data folder * Replace get_all_state with custom method to avoid the cache * OoT ROM: increment item counter before setting incoming item/player values to 0, preventing desync issues * set data_version to 0 * make Kokiri Sword shuffle off by default * reenable "Random Choice" for various cosmetic options * kill Ruto's Letter turnin if open fountain also fix for shopsanity * place Buy Goron/Zora Tunic first in shop shuffle * make ice traps appear as other items instead of breaking generation * managed to break ice traps on non-major-only * only handle ice traps if they are on * fix shopsanity for non-oot games, and write player name instead of player number * light arrows hint uses player name instead of player number * Reenable "skip child zelda" option * fix entrances_based_rules * fix ganondorf hint if starting with light arrows * fix dungeonitem shuffle and shopsanity interaction * remove has_all_of, has_any_of, count_of in BaseClasses, replace usage with has_all, has_any, has_group * force local giveable item on ZL if skip_child_zelda and shuffle_song_items is any * keep bosses and bombchu bowling chus out of data package * revert workaround for infinite recursion and fix it properly * fix shared shop id caches during patching process * fix shop text box overflows, as much as possible * add default oot host.yaml option * add .apz5, .n64, .z64 to gitignore * Properly document and name all (functioning) OOT options * clean up some imports * remove unnecessary files from oot's data * fix typo in gitignore * readd the Compress and Decompress utilities, since they are needed for generation * cleanup of imports and some minor optimizations * increase shop offset for item IDs to 0xCB * remove shop item AP ids entirely * prevent triforce pieces for other players from being received by yourself * add "excluded" property to Location * Hint system adapted and reenabled; hints still unseeded * make hints deterministic with lists instead of sets * do not allow hints to point to Light Arrows on non-vanilla bridge * foreign locations hint as their full name in OoT rather than their region * checkedLocations now stores hint names by player ID, so that the same location in different worlds can have hints associated * consolidate versioning in Utils * ice traps appear as major items rather than any progression item * set prescription and claim check as defaults for adult trade item settings * add oot options to playerSettings * allow case-insensitive logic tricks in yaml * fix oot shopsanity option formatting * Write OoT override info even if local item, enabling local checks to show up immediately in the client * implement CollectionState.can_live_dmg for oot glitched logic * filter item names for invalid characters when patching shops * make ice traps appear according to the settings of the world they are shuffled into, rather than the original world * set hidden-spoiler items and locations with Shop items to events * make GF carpenters, Gerudo Card, Malon, ZL, and Impa events if the relevant settings are enabled, preventing them from appearing in the client on game start * Fix oot Glitched and No Logic generation * fix indenting * Greatly reduce displayed cosmetic options * Change oot data version to 1 * add apz5 distribution to webhost * print player name if an ALttP dungeon contains a good item for OoT world * delete unneeded commented code * remove OcarinaSongs import to satisfy lint
2021-09-02 12:35:05 +00:00
from itertools import chain
class Address():
prev_address = None
def __init__(self, address=None, size=4, mask=0xFFFFFFFF, max=None, choices=None, value=None):
if address is None:
self.address = Address.prev_address
else:
self.address = address
self.value = value
self.size = size
self.choices = choices
self.mask = mask
Address.prev_address = self.address + self.size
self.bit_offset = 0
while mask & 1 == 0:
mask = mask >> 1
self.bit_offset += 1
if max is None:
self.max = mask
else:
self.max = max
def get_value(self, default=0):
if self.value is None:
return default
return self.value
def get_value_raw(self):
if self.value is None:
return None
value = self.value
if self.choices is not None:
value = self.choices[value]
if not isinstance(value, int):
raise ValueError("Invalid value type '%s'" % str(value))
if isinstance(value, bool):
value = 1 if value else 0
if value > self.max:
value = self.max
value = (value << self.bit_offset) & self.mask
return value
def set_value_raw(self, value):
if value is None:
self.value = None
return
if not isinstance(value, int):
raise ValueError("Invalid value type '%s'" % str(value))
value = (value & self.mask) >> self.bit_offset
if value > self.max:
value = self.max
if self.choices is not None:
for choice_name, choice_value in self.choices.items():
if choice_value == value:
value = choice_name
break
self.value = value
def get_writes(self, save_context):
if self.value is None:
return
value = self.get_value_raw()
if value is None:
return
values = zip(Address.to_bytes(value, self.size),
Address.to_bytes(self.mask, self.size))
for i, (byte, mask) in enumerate(values):
if mask == 0:
continue
if mask == 0xFF:
save_context.write_byte(self.address + i, byte)
else:
save_context.write_bits(self.address + i, byte, mask=mask)
def to_bytes(value, size):
ret = []
for _ in range(size):
ret.insert(0, value & 0xFF)
value = value >> 8
return ret
class SaveContext():
def __init__(self):
self.save_bits = {}
self.save_bytes = {}
self.addresses = self.get_save_context_addresses()
# will set the bits of value to the offset in the save (or'ing them with what is already there)
def write_bits(self, address, value, mask=None, predicate=None):
if predicate and not predicate(value):
return
if mask is not None:
value = value & mask
if address in self.save_bytes:
old_val = self.save_bytes[address]
if mask is not None:
old_val &= ~mask
value = old_val | value
self.write_byte(address, value, predicate)
elif address in self.save_bits:
if mask is not None:
self.save_bits[address] &= ~mask
self.save_bits[address] |= value
else:
self.save_bits[address] = value
# will overwrite the byte at offset with the given value
def write_byte(self, address, value, predicate=None):
if predicate and not predicate(value):
return
if address in self.save_bits:
del self.save_bits[address]
self.save_bytes[address] = value
# will overwrite the byte at offset with the given value
def write_bytes(self, address, bytes, predicate=None):
for i, value in enumerate(bytes):
self.write_byte(address + i, value, predicate)
def write_save_entry(self, address):
if isinstance(address, dict):
for name, sub_address in address.items():
self.write_save_entry(sub_address)
elif isinstance(address, list):
for sub_address in address:
self.write_save_entry(sub_address)
else:
address.get_writes(self)
def set_ammo_max(self):
ammo_maxes = {
'stick' : ('stick_upgrade', [10, 10, 20, 30]),
'nut' : ('nut_upgrade', [20, 20, 30, 40]),
'bomb' : ('bomb_bag', [00, 20, 30, 40]),
'bow' : ('quiver', [00, 30, 40, 50]),
'slingshot' : ('bullet_bag', [00, 30, 40, 50]),
'rupees' : ('wallet', [99, 200, 500, 999]),
}
for ammo, (upgrade, maxes) in ammo_maxes.items():
upgrade_count = self.addresses['upgrades'][upgrade].get_value()
try:
ammo_max = maxes[upgrade_count]
except IndexError:
ammo_max = maxes[-1]
if ammo == 'rupees':
self.addresses[ammo].max = ammo_max
else:
self.addresses['ammo'][ammo].max = ammo_max
# will overwrite the byte at offset with the given value
def write_save_table(self, rom):
self.set_ammo_max()
for name, address in self.addresses.items():
self.write_save_entry(address)
save_table = []
for address, value in self.save_bits.items():
if value != 0:
save_table += [(address & 0xFF00) >> 8, address & 0xFF, 0x00, value]
for address, value in self.save_bytes.items():
save_table += [(address & 0xFF00) >> 8, address & 0xFF, 0x01, value]
save_table += [0x00,0x00,0x00,0x00]
table_len = len(save_table)
if table_len > 0x400:
raise Exception("The Initial Save Table has exceeded its maximum capacity: 0x%03X/0x400" % table_len)
rom.write_bytes(rom.sym('INITIAL_SAVE_DATA'), save_table)
def give_bottle(self, item, count):
for bottle_id in range(4):
item_slot = 'bottle_%d' % (bottle_id + 1)
if self.addresses['item_slot'][item_slot].get_value(0xFF) != 0xFF:
continue
self.addresses['item_slot'][item_slot].value = SaveContext.bottle_types[item]
count -= 1
if count == 0:
return
def give_health(self, health):
health += self.addresses['health_capacity'].get_value(0x30) / 0x10
health += self.addresses['quest']['heart_pieces'].get_value() / 4
self.addresses['health_capacity'].value = int(health) * 0x10
self.addresses['health'].value = int(health) * 0x10
self.addresses['quest']['heart_pieces'].value = int((health % 1) * 4)
def give_raw_item(self, item):
if item.endswith(')'):
item_base, count = item[:-1].split(' (', 1)
if count.isdigit():
return self.give_item(item_base, count=int(count))
return self.give_item(item)
def give_item(self, item, count=1):
if item in SaveContext.bottle_types:
self.give_bottle(item, count)
elif item in ["Piece of Heart", "Piece of Heart (Treasure Chest Game)"]:
self.give_health(count / 4)
elif item == "Heart Container":
self.give_health(count)
elif item == "Bombchu Item":
self.give_bombchu_item()
elif item in SaveContext.save_writes_table:
for address, value in SaveContext.save_writes_table[item].items():
if value is None:
value = count
elif isinstance(value, list):
value = value[min(len(value), count) - 1]
elif isinstance(value, bool):
value = 1 if value else 0
address_value = self.addresses
prev_sub_address = 'Save Context'
for sub_address in address.split('.'):
if sub_address not in address_value:
raise ValueError('Unknown key %s in %s of SaveContext' % (sub_address, prev_sub_address))
if isinstance(address_value, list):
sub_address = int(sub_address)
address_value = address_value[sub_address]
prev_sub_address = sub_address
if not isinstance(address_value, Address):
raise ValueError('%s does not resolve to an Address in SaveContext' % (sub_address))
if isinstance(value, int) and value < address_value.get_value():
continue
address_value.value = value
else:
raise ValueError("Cannot give unknown starting item %s" % item)
def give_bombchu_item(self):
self.give_item("Bombchus", 0)
def equip_default_items(self, age):
self.equip_items(age, 'equips_' + age)
def equip_current_items(self, age):
self.equip_items(age, 'equips')
def equip_items(self, age, equip_type):
if age not in ['child', 'adult']:
raise ValueError("Age must be 'adult' or 'child', not %s" % age)
if equip_type not in ['equips', 'equips_child', 'equips_adult']:
raise ValueError("Equip type must be 'equips', 'equips_child' or 'equips_adult', not %s" % equip_type)
age = 'equips_' + age
c_buttons = list(self.addresses[age]['button_slots'].keys())
for item_slot in SaveContext.equipable_items[age]['items']:
item = self.addresses['item_slot'][item_slot].get_value('none')
if item != 'none':
c_button = c_buttons.pop()
self.addresses[equip_type]['button_slots'][c_button].value = item_slot
self.addresses[equip_type]['button_items'][c_button].value = item
if not c_buttons:
break
for equip_item, equip_addresses in self.addresses[age]['equips'].items():
for item in SaveContext.equipable_items[age][equip_item]:
if self.addresses['equip_items'][item].get_value():
item_value = self.addresses['equip_items'][item].get_value_raw()
self.addresses[equip_type]['equips'][equip_item].set_value_raw(item_value)
if equip_item == 'tunic':
self.addresses[equip_type]['equips'][equip_item].value = 1
if equip_item == 'sword':
self.addresses[equip_type]['button_items']['b'].value = item
break
def get_save_context_addresses(self):
return {
'entrance_index' : Address(0x0000, size=4),
'link_age' : Address(size=4, max=1),
'unk_00' : Address(size=2),
'cutscene_index' : Address(size=2),
'time_of_day' : Address(size=2),
'unk_01' : Address(size=2),
'night_flag' : Address(size=4, max=1),
'unk_02' : Address(size=8),
'id' : Address(size=6),
'deaths' : Address(size=2),
'file_name' : Address(size=8),
'n64dd_flag' : Address(size=2),
'health_capacity' : Address(size=2, max=0x140),
'health' : Address(size=2, max=0x140),
'magic_level' : Address(size=1, max=2),
'magic' : Address(size=1, max=0x60),
'rupees' : Address(size=2),
'bgs_hits_left' : Address(size=2),
'navi_timer' : Address(size=2),
'magic_acquired' : Address(size=1, max=1),
'unk_03' : Address(size=1),
'double_magic' : Address(size=1, max=1),
'double_defense' : Address(size=1, max=1),
'bgs_flag' : Address(size=1, max=1),
'unk_05' : Address(size=1),
# Equiped Items
'equips_child' : {
'button_items' : {
'b' : Address(size=1, choices=SaveContext.item_id_map),
'left' : Address(size=1, choices=SaveContext.item_id_map),
'down' : Address(size=1, choices=SaveContext.item_id_map),
'right' : Address(size=1, choices=SaveContext.item_id_map),
},
'button_slots' : {
'left' : Address(size=1, choices=SaveContext.slot_id_map),
'down' : Address(size=1, choices=SaveContext.slot_id_map),
'right' : Address(size=1, choices=SaveContext.slot_id_map),
},
'equips' : {
'sword' : Address(0x0048, size=2, mask=0x000F),
'shield' : Address(0x0048, size=2, mask=0x00F0),
'tunic' : Address(0x0048, size=2, mask=0x0F00),
'boots' : Address(0x0048, size=2, mask=0xF000),
},
},
'equips_adult' : {
'button_items' : {
'b' : Address(size=1, choices=SaveContext.item_id_map),
'left' : Address(size=1, choices=SaveContext.item_id_map),
'down' : Address(size=1, choices=SaveContext.item_id_map),
'right' : Address(size=1, choices=SaveContext.item_id_map),
},
'button_slots' : {
'left' : Address(size=1, choices=SaveContext.slot_id_map),
'down' : Address(size=1, choices=SaveContext.slot_id_map),
'right' : Address(size=1, choices=SaveContext.slot_id_map),
},
'equips' : {
'sword' : Address(0x0052, size=2, mask=0x000F),
'shield' : Address(0x0052, size=2, mask=0x00F0),
'tunic' : Address(0x0052, size=2, mask=0x0F00),
'boots' : Address(0x0052, size=2, mask=0xF000),
},
},
'unk_06' : Address(size=0x12),
'scene_index' : Address(size=2),
'equips' : {
'button_items' : {
'b' : Address(size=1, choices=SaveContext.item_id_map),
'left' : Address(size=1, choices=SaveContext.item_id_map),
'down' : Address(size=1, choices=SaveContext.item_id_map),
'right' : Address(size=1, choices=SaveContext.item_id_map),
},
'button_slots' : {
'left' : Address(size=1, choices=SaveContext.slot_id_map),
'down' : Address(size=1, choices=SaveContext.slot_id_map),
'right' : Address(size=1, choices=SaveContext.slot_id_map),
},
'equips' : {
'sword' : Address(0x0070, size=2, mask=0x000F, max=3),
'shield' : Address(0x0070, size=2, mask=0x00F0, max=3),
'tunic' : Address(0x0070, size=2, mask=0x0F00, max=3),
'boots' : Address(0x0070, size=2, mask=0xF000, max=3),
},
},
'unk_07' : Address(size=2),
# Item Slots
'item_slot' : {
'stick' : Address(size=1, choices=SaveContext.item_id_map),
'nut' : Address(size=1, choices=SaveContext.item_id_map),
'bomb' : Address(size=1, choices=SaveContext.item_id_map),
'bow' : Address(size=1, choices=SaveContext.item_id_map),
'fire_arrow' : Address(size=1, choices=SaveContext.item_id_map),
'dins_fire' : Address(size=1, choices=SaveContext.item_id_map),
'slingshot' : Address(size=1, choices=SaveContext.item_id_map),
'ocarina' : Address(size=1, choices=SaveContext.item_id_map),
'bombchu' : Address(size=1, choices=SaveContext.item_id_map),
'hookshot' : Address(size=1, choices=SaveContext.item_id_map),
'ice_arrow' : Address(size=1, choices=SaveContext.item_id_map),
'farores_wind' : Address(size=1, choices=SaveContext.item_id_map),
'boomerang' : Address(size=1, choices=SaveContext.item_id_map),
'lens' : Address(size=1, choices=SaveContext.item_id_map),
'beans' : Address(size=1, choices=SaveContext.item_id_map),
'hammer' : Address(size=1, choices=SaveContext.item_id_map),
'light_arrow' : Address(size=1, choices=SaveContext.item_id_map),
'nayrus_love' : Address(size=1, choices=SaveContext.item_id_map),
'bottle_1' : Address(size=1, choices=SaveContext.item_id_map),
'bottle_2' : Address(size=1, choices=SaveContext.item_id_map),
'bottle_3' : Address(size=1, choices=SaveContext.item_id_map),
'bottle_4' : Address(size=1, choices=SaveContext.item_id_map),
'adult_trade' : Address(size=1, choices=SaveContext.item_id_map),
'child_trade' : Address(size=1, choices=SaveContext.item_id_map),
},
# Item Ammo
'ammo' : {
'stick' : Address(size=1),
'nut' : Address(size=1),
'bomb' : Address(size=1),
'bow' : Address(size=1),
'fire_arrow' : Address(size=1, max=0),
'dins_fire' : Address(size=1, max=0),
'slingshot' : Address(size=1),
'ocarina' : Address(size=1, max=0),
'bombchu' : Address(size=1, max=50),
'hookshot' : Address(size=1, max=0),
'ice_arrow' : Address(size=1, max=0),
'farores_wind' : Address(size=1, max=0),
'boomerang' : Address(size=1, max=0),
'lens' : Address(size=1, max=0),
'beans' : Address(size=1, max=10),
},
'magic_beans_sold' : Address(size=1, max=10),
# Equipment
'equip_items' : {
'kokiri_sword' : Address(0x009C, size=2, mask=0x0001),
'master_sword' : Address(0x009C, size=2, mask=0x0002),
'biggoron_sword' : Address(0x009C, size=2, mask=0x0004),
'broken_knife' : Address(0x009C, size=2, mask=0x0008),
'deku_shield' : Address(0x009C, size=2, mask=0x0010),
'hylian_shield' : Address(0x009C, size=2, mask=0x0020),
'mirror_shield' : Address(0x009C, size=2, mask=0x0040),
'kokiri_tunic' : Address(0x009C, size=2, mask=0x0100),
'goron_tunic' : Address(0x009C, size=2, mask=0x0200),
'zora_tunic' : Address(0x009C, size=2, mask=0x0400),
'kokiri_boots' : Address(0x009C, size=2, mask=0x1000),
'iron_boots' : Address(0x009C, size=2, mask=0x2000),
'hover_boots' : Address(0x009C, size=2, mask=0x4000),
},
'unk_08' : Address(size=2),
# Upgrades
'upgrades' : {
'quiver' : Address(0x00A0, mask=0x00000007, max=3),
'bomb_bag' : Address(0x00A0, mask=0x00000038, max=3),
'strength_upgrade' : Address(0x00A0, mask=0x000001C0, max=3),
'diving_upgrade' : Address(0x00A0, mask=0x00000E00, max=2),
'wallet' : Address(0x00A0, mask=0x00003000, max=3),
'bullet_bag' : Address(0x00A0, mask=0x0001C000, max=3),
'stick_upgrade' : Address(0x00A0, mask=0x000E0000, max=3),
'nut_upgrade' : Address(0x00A0, mask=0x00700000, max=3),
},
# Medallions
'quest' : {
'medallions' : {
'forest' : Address(0x00A4, mask=0x00000001),
'fire' : Address(0x00A4, mask=0x00000002),
'water' : Address(0x00A4, mask=0x00000004),
'spirit' : Address(0x00A4, mask=0x00000008),
'shadow' : Address(0x00A4, mask=0x00000010),
'light' : Address(0x00A4, mask=0x00000020),
},
'songs' : {
'minuet_of_forest' : Address(0x00A4, mask=0x00000040),
'bolero_of_fire' : Address(0x00A4, mask=0x00000080),
'serenade_of_water' : Address(0x00A4, mask=0x00000100),
'requiem_of_spirit' : Address(0x00A4, mask=0x00000200),
'nocturne_of_shadow' : Address(0x00A4, mask=0x00000400),
'prelude_of_light' : Address(0x00A4, mask=0x00000800),
'zeldas_lullaby' : Address(0x00A4, mask=0x00001000),
'eponas_song' : Address(0x00A4, mask=0x00002000),
'sarias_song' : Address(0x00A4, mask=0x00004000),
'suns_song' : Address(0x00A4, mask=0x00008000),
'song_of_time' : Address(0x00A4, mask=0x00010000),
'song_of_storms' : Address(0x00A4, mask=0x00020000),
},
'stones' : {
'kokiris_emerald' : Address(0x00A4, mask=0x00040000),
'gorons_ruby' : Address(0x00A4, mask=0x00080000),
'zoras_sapphire' : Address(0x00A4, mask=0x00100000),
},
'stone_of_agony' : Address(0x00A4, mask=0x00200000),
'gerudos_card' : Address(0x00A4, mask=0x00400000),
'gold_skulltula' : Address(0x00A4, mask=0x00800000),
'heart_pieces' : Address(0x00A4, mask=0xFF000000),
},
# Dungeon Items
'dungeon_items' : {
'deku' : {
'boss_key' : Address(0x00A8, size=1, mask=0x01),
'compass' : Address(0x00A8, size=1, mask=0x02),
'map' : Address(0x00A8, size=1, mask=0x04),
},
'dodongo' : {
'boss_key' : Address(0x00A9, size=1, mask=0x01),
'compass' : Address(0x00A9, size=1, mask=0x02),
'map' : Address(0x00A9, size=1, mask=0x04),
},
'jabu' : {
'boss_key' : Address(0x00AA, size=1, mask=0x01),
'compass' : Address(0x00AA, size=1, mask=0x02),
'map' : Address(0x00AA, size=1, mask=0x04),
},
'forest' : {
'boss_key' : Address(0x00AB, size=1, mask=0x01),
'compass' : Address(0x00AB, size=1, mask=0x02),
'map' : Address(0x00AB, size=1, mask=0x04),
},
'fire' : {
'boss_key' : Address(0x00AC, size=1, mask=0x01),
'compass' : Address(0x00AC, size=1, mask=0x02),
'map' : Address(0x00AC, size=1, mask=0x04),
},
'water' : {
'boss_key' : Address(0x00AD, size=1, mask=0x01),
'compass' : Address(0x00AD, size=1, mask=0x02),
'map' : Address(0x00AD, size=1, mask=0x04),
},
'spirit' : {
'boss_key' : Address(0x00AE, size=1, mask=0x01),
'compass' : Address(0x00AE, size=1, mask=0x02),
'map' : Address(0x00AE, size=1, mask=0x04),
},
'shadow' : {
'boss_key' : Address(0x00AF, size=1, mask=0x01),
'compass' : Address(0x00AF, size=1, mask=0x02),
'map' : Address(0x00AF, size=1, mask=0x04),
},
'botw' : {
'boss_key' : Address(0x00B0, size=1, mask=0x01),
'compass' : Address(0x00B0, size=1, mask=0x02),
'map' : Address(0x00B0, size=1, mask=0x04),
},
'ice' : {
'boss_key' : Address(0x00B1, size=1, mask=0x01),
'compass' : Address(0x00B1, size=1, mask=0x02),
'map' : Address(0x00B1, size=1, mask=0x04),
},
'gt' : {
'boss_key' : Address(0x00B2, size=1, mask=0x01),
'compass' : Address(0x00B2, size=1, mask=0x02),
'map' : Address(0x00B2, size=1, mask=0x04),
},
'gtg' : {
'boss_key' : Address(0x00B3, size=1, mask=0x01),
'compass' : Address(0x00B3, size=1, mask=0x02),
'map' : Address(0x00B3, size=1, mask=0x04),
},
'fortress' : {
'boss_key' : Address(0x00B4, size=1, mask=0x01),
'compass' : Address(0x00B4, size=1, mask=0x02),
'map' : Address(0x00B4, size=1, mask=0x04),
},
'gc' : {
'boss_key' : Address(0x00B5, size=1, mask=0x01),
'compass' : Address(0x00B5, size=1, mask=0x02),
'map' : Address(0x00B5, size=1, mask=0x04),
},
'unused' : Address(size=6),
},
'keys' : {
'deku' : Address(size=1),
'dodongo' : Address(size=1),
'jabu' : Address(size=1),
'forest' : Address(size=1),
'fire' : Address(size=1),
'water' : Address(size=1),
'spirit' : Address(size=1),
'shadow' : Address(size=1),
'botw' : Address(size=1),
'ice' : Address(size=1),
'gt' : Address(size=1),
'gtg' : Address(size=1),
'fortress' : Address(size=1),
'gc' : Address(size=1),
'unused' : Address(size=5),
},
'defense_hearts' : Address(size=1, max=20),
'gs_tokens' : Address(size=2, max=100),
'triforce_pieces' : Address(0xD4 + 0x1C * 0x48 + 0x10, size=4), # Unused word in scene x48
}
item_id_map = {
'none' : 0xFF,
'stick' : 0x00,
'nut' : 0x01,
'bomb' : 0x02,
'bow' : 0x03,
'fire_arrow' : 0x04,
'dins_fire' : 0x05,
'slingshot' : 0x06,
'fairy_ocarina' : 0x07,
'ocarina_of_time' : 0x08,
'bombchu' : 0x09,
'hookshot' : 0x0A,
'longshot' : 0x0B,
'ice_arrow' : 0x0C,
'farores_wind' : 0x0D,
'boomerang' : 0x0E,
'lens' : 0x0F,
'beans' : 0x10,
'hammer' : 0x11,
'light_arrow' : 0x12,
'nayrus_love' : 0x13,
'bottle' : 0x14,
'red_potion' : 0x15,
'green_potion' : 0x16,
'blue_potion' : 0x17,
'fairy' : 0x18,
'fish' : 0x19,
'milk' : 0x1A,
'letter' : 0x1B,
'blue_fire' : 0x1C,
'bug' : 0x1D,
'big_poe' : 0x1E,
'half_milk' : 0x1F,
'poe' : 0x20,
'weird_egg' : 0x21,
'chicken' : 0x22,
'zeldas_letter' : 0x23,
'keaton_mask' : 0x24,
'skull_mask' : 0x25,
'spooky_mask' : 0x26,
'bunny_hood' : 0x27,
'goron_mask' : 0x28,
'zora_mask' : 0x29,
'gerudo_mask' : 0x2A,
'mask_of_truth' : 0x2B,
'sold_out' : 0x2C,
'pocket_egg' : 0x2D,
'pocket_cucco' : 0x2E,
'cojiro' : 0x2F,
'odd_mushroom' : 0x30,
'odd_potion' : 0x31,
'poachers_saw' : 0x32,
'broken_gorons_sword' : 0x33,
'prescription' : 0x34,
'eyeball_frog' : 0x35,
'eye_drops' : 0x36,
'claim_check' : 0x37,
'bow_fire_arrow' : 0x38,
'bow_ice_arrow' : 0x39,
'bow_light_arrow' : 0x3A,
'kokiri_sword' : 0x3B,
'master_sword' : 0x3C,
'biggoron_sword' : 0x3D,
'deku_shield' : 0x3E,
'hylian_shield' : 0x3F,
'mirror_shield' : 0x40,
'kokiri_tunic' : 0x41,
'goron_tunic' : 0x42,
'zora_tunic' : 0x43,
'kokiri_boots' : 0x44,
'iron_boots' : 0x45,
'hover_boots' : 0x46,
'bullet_bag_30' : 0x47,
'bullet_bag_40' : 0x48,
'bullet_bag_50' : 0x49,
'quiver_30' : 0x4A,
'quiver_40' : 0x4B,
'quiver_50' : 0x4C,
'bomb_bag_20' : 0x4D,
'bomb_bag_30' : 0x4E,
'bomb_bag_40' : 0x4F,
'gorons_bracelet' : 0x40,
'silver_gauntlets' : 0x41,
'golden_gauntlets' : 0x42,
'silver_scale' : 0x43,
'golden_scale' : 0x44,
'broken_giants_knife' : 0x45,
'adults_wallet' : 0x46,
'giants_wallet' : 0x47,
'deku_seeds' : 0x48,
'fishing_pole' : 0x49,
'minuet' : 0x4A,
'bolero' : 0x4B,
'serenade' : 0x4C,
'requiem' : 0x4D,
'nocturne' : 0x4E,
'prelude' : 0x4F,
'zeldas_lullaby' : 0x50,
'eponas_song' : 0x51,
'sarias_song' : 0x52,
'suns_song' : 0x53,
'song_of_time' : 0x54,
'song_of_storms' : 0x55,
'forest_medallion' : 0x56,
'fire_medallion' : 0x57,
'water_medallion' : 0x58,
'spirit_medallion' : 0x59,
'shadow_medallion' : 0x5A,
'light_medallion' : 0x5B,
'kokiris_emerald' : 0x5C,
'gorons_ruby' : 0x5D,
'zoras_sapphire' : 0x5E,
'stone_of_agony' : 0x5F,
'gerudos_card' : 0x60,
'gold_skulltula' : 0x61,
'heart_container' : 0x62,
'piece_of_heart' : 0x63,
'boss_key' : 0x64,
'compass' : 0x65,
'dungeon_map' : 0x66,
'small_key' : 0x67,
}
slot_id_map = {
'stick' : 0x00,
'nut' : 0x01,
'bomb' : 0x02,
'bow' : 0x03,
'fire_arrow' : 0x04,
'dins_fire' : 0x05,
'slingshot' : 0x06,
'ocarina' : 0x07,
'bombchu' : 0x08,
'hookshot' : 0x09,
'ice_arrow' : 0x0A,
'farores_wind' : 0x0B,
'boomerang' : 0x0C,
'lens' : 0x0D,
'beans' : 0x0E,
'hammer' : 0x0F,
'light_arrow' : 0x10,
'nayrus_love' : 0x11,
'bottle_1' : 0x12,
'bottle_2' : 0x13,
'bottle_3' : 0x14,
'bottle_4' : 0x15,
'adult_trade' : 0x16,
'child_trade' : 0x17,
}
bottle_types = {
"Bottle" : 'bottle',
"Bottle with Red Potion" : 'red_potion',
"Bottle with Green Potion" : 'green_potion',
"Bottle with Blue Potion" : 'blue_potion',
"Bottle with Fairy" : 'fairy',
"Bottle with Fish" : 'fish',
"Bottle with Milk" : 'milk',
"Rutos Letter" : 'letter',
"Bottle with Blue Fire" : 'blue_fire',
"Bottle with Bugs" : 'bug',
"Bottle with Big Poe" : 'big_poe',
"Bottle with Milk (Half)" : 'half_milk',
"Bottle with Poe" : 'poe',
}
save_writes_table = {
"Deku Stick Capacity": {
'item_slot.stick' : 'stick',
'upgrades.stick_upgrade' : [2,3],
},
"Deku Sticks": {
'item_slot.stick' : 'stick',
'upgrades.stick_upgrade' : 1,
'ammo.stick' : None,
},
"Deku Nut Capacity": {
'item_slot.nut' : 'nut',
'upgrades.nut_upgrade' : [2,3],
},
"Deku Nuts": {
'item_slot.nut' : 'nut',
'upgrades.nut_upgrade' : 1,
'ammo.nut' : None,
},
"Bomb Bag": {
'item_slot.bomb' : 'bomb',
'upgrades.bomb_bag' : None,
},
"Bombs" : {
'ammo.bomb' : None,
},
"Bombchus" : {
'item_slot.bombchu' : 'bombchu',
'ammo.bombchu' : None,
},
"Bow" : {
'item_slot.bow' : 'bow',
'upgrades.quiver' : None,
},
"Arrows" : {
'ammo.bow' : None,
},
"Slingshot" : {
'item_slot.slingshot' : 'slingshot',
'upgrades.bullet_bag' : None,
},
"Deku Seeds" : {
'ammo.slingshot' : None,
},
"Magic Bean" : {
'item_slot.beans' : 'beans',
'ammo.beans' : None,
'magic_beans_sold' : None,
},
"Fire Arrows" : {'item_slot.fire_arrow' : 'fire_arrow'},
"Ice Arrows" : {'item_slot.ice_arrow' : 'ice_arrow'},
"Light Arrows" : {'item_slot.light_arrow' : 'light_arrow'},
"Dins Fire" : {'item_slot.dins_fire' : 'dins_fire'},
"Farores Wind" : {'item_slot.farores_wind' : 'farores_wind'},
"Nayrus Love" : {'item_slot.nayrus_love' : 'nayrus_love'},
"Ocarina" : {'item_slot.ocarina' : ['fairy_ocarina', 'ocarina_of_time']},
"Progressive Hookshot" : {'item_slot.hookshot' : ['hookshot', 'longshot']},
"Boomerang" : {'item_slot.boomerang' : 'boomerang'},
"Lens of Truth" : {'item_slot.lens' : 'lens'},
"Megaton Hammer" : {'item_slot.hammer' : 'hammer'},
"Pocket Egg" : {'item_slot.adult_trade' : 'pocket_egg'},
"Pocket Cucco" : {'item_slot.adult_trade' : 'pocket_cucco'},
"Cojiro" : {'item_slot.adult_trade' : 'cojiro'},
"Odd Mushroom" : {'item_slot.adult_trade' : 'odd_mushroom'},
"Poachers Saw" : {'item_slot.adult_trade' : 'poachers_saw'},
"Broken Sword" : {'item_slot.adult_trade' : 'broken_knife'},
"Prescription" : {'item_slot.adult_trade' : 'prescription'},
"Eyeball Frog" : {'item_slot.adult_trade' : 'eyeball_frog'},
"Eyedrops" : {'item_slot.adult_trade' : 'eye_drops'},
"Claim Check" : {'item_slot.adult_trade' : 'claim_check'},
"Weird Egg" : {'item_slot.child_trade' : 'weird_egg'},
"Chicken" : {'item_slot.child_trade' : 'chicken'},
"Zeldas Letter" : {'item_slot.child_trade' : 'zeldas_letter'},
"Goron Tunic" : {'equip_items.goron_tunic' : True},
"Zora Tunic" : {'equip_items.zora_tunic' : True},
"Iron Boots" : {'equip_items.iron_boots' : True},
"Hover Boots" : {'equip_items.hover_boots' : True},
"Deku Shield" : {'equip_items.deku_shield' : True},
"Hylian Shield" : {'equip_items.hylian_shield' : True},
"Mirror Shield" : {'equip_items.mirror_shield' : True},
"Kokiri Sword" : {'equip_items.kokiri_sword' : True},
"Master Sword" : {'equip_items.master_sword' : True},
"Giants Knife" : {
'equip_items.biggoron_sword' : True,
'bgs_hits_left' : 8,
},
"Biggoron Sword" : {
'equip_items.biggoron_sword' : True,
'bgs_flag' : True,
'bgs_hits_left' : 1,
},
"Gerudo Membership Card" : {'quest.gerudos_card' : True},
"Stone of Agony" : {'quest.stone_of_agony' : True},
"Zeldas Lullaby" : {'quest.songs.zeldas_lullaby' : True},
"Eponas Song" : {'quest.songs.eponas_song' : True},
"Sarias Song" : {'quest.songs.sarias_song' : True},
"Suns Song" : {'quest.songs.suns_song' : True},
"Song of Time" : {'quest.songs.song_of_time' : True},
"Song of Storms" : {'quest.songs.song_of_storms' : True},
"Minuet of Forest" : {'quest.songs.minuet_of_forest' : True},
"Bolero of Fire" : {'quest.songs.bolero_of_fire' : True},
"Serenade of Water" : {'quest.songs.serenade_of_water' : True},
"Requiem of Spirit" : {'quest.songs.requiem_of_spirit' : True},
"Nocturne of Shadow" : {'quest.songs.nocturne_of_shadow' : True},
"Prelude of Light" : {'quest.songs.prelude_of_light' : True},
"Kokiri Emerald" : {'quest.stones.kokiris_emerald' : True},
"Goron Ruby" : {'quest.stones.gorons_ruby' : True},
"Zora Sapphire" : {'quest.stones.zoras_sapphire' : True},
"Light Medallion" : {'quest.medallions.light' : True},
"Forest Medallion" : {'quest.medallions.forest' : True},
"Fire Medallion" : {'quest.medallions.fire' : True},
"Water Medallion" : {'quest.medallions.water' : True},
"Spirit Medallion" : {'quest.medallions.spirit' : True},
"Shadow Medallion" : {'quest.medallions.shadow' : True},
"Progressive Strength Upgrade" : {'upgrades.strength_upgrade' : None},
"Progressive Scale" : {'upgrades.diving_upgrade' : None},
"Progressive Wallet" : {'upgrades.wallet' : None},
"Gold Skulltula Token" : {
'quest.gold_skulltula' : True,
'gs_tokens' : None,
},
"Double Defense" : {
'double_defense' : True,
'defense_hearts' : 20,
},
"Magic Meter" : {
'magic_acquired' : True,
'magic' : [0x30, 0x60],
'magic_level' : None,
'double_magic' : [False, True],
},
"Rupee" : {'rupees' : None},
"Rupees" : {'rupees' : None},
"Magic Bean Pack" : {
'item_slot.beans' : 'beans',
'ammo.beans' : 10
},
"Triforce Piece" : {'triforce_pieces': None},
}
giveable_items = set(chain(save_writes_table.keys(), bottle_types.keys(),
["Piece of Heart", "Piece of Heart (Treasure Chest Game)", "Heart Container", "Rupee (1)"]))
equipable_items = {
'equips_adult' : {
'items': [
'hookshot',
'hammer',
'bomb',
'bow',
'nut',
'lens',
'farores_wind',
'dins_fire',
'bombchu',
'nayrus_love',
'adult_trade',
'bottle_1',
'bottle_2',
'bottle_3',
'bottle_4',
],
'sword' : [
'biggoron_sword',
'master_sword',
],
'shield' : [
'mirror_shield',
'hylian_shield',
],
'tunic' : [
'goron_tunic',
'zora_tunic',
'kokiri_tunic',
],
'boots' : [
'kokiri_boots'
],
},
'equips_child' : {
'items': [
'bomb',
'boomerang',
'slingshot',
'stick',
'nut',
'lens',
'farores_wind',
'dins_fire',
'bombchu',
'nayrus_love',
'beans',
'child_trade',
'bottle_1',
'bottle_2',
'bottle_3',
'bottle_4',
],
'sword' : [
'kokiri_sword',
],
'shield' : [
'deku_shield',
'hylian_shield',
],
'tunic' : [
'kokiri_tunic',
],
'boots' : [
'kokiri_boots',
],
}
}