Ocarina of Time: reduce memory use by 64 MiB for each OoT world past the first

Ocarina of Time: limit parallel output to 2, to not waste memory that doesn't benefit speed
Ocarina of Time: remove swarm of os.chdir()
This commit is contained in:
Fabian Dill 2021-09-03 12:50:26 +02:00
parent 51c38fc628
commit 1b27fc495f
5 changed files with 145 additions and 146 deletions

View File

@ -19,7 +19,7 @@ from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain from Main import main as ERmain
from Main import get_seed, seeddigits from Main import get_seed, seeddigits
import Options import Options
from worlds.alttp.Items import item_name_groups, item_table from worlds.alttp.Items import item_table
from worlds.alttp import Bosses from worlds.alttp import Bosses
from worlds.alttp.Text import TextTable from worlds.alttp.Text import TextTable
from worlds.alttp.Regions import location_table, key_drop_data from worlds.alttp.Regions import location_table, key_drop_data

View File

@ -82,7 +82,6 @@ def page_not_found(err):
return render_template('404.html'), 404 return render_template('404.html'), 404
# Player settings pages # Player settings pages
@app.route('/games/<string:game>/player-settings') @app.route('/games/<string:game>/player-settings')
def player_settings(game): def player_settings(game):
@ -176,10 +175,12 @@ def hostRoom(room: UUID):
return render_template("hostRoom.html", room=room) return render_template("hostRoom.html", room=room)
@app.route('/hosted/<suuid:room>', methods=['GET', 'POST']) @app.route('/hosted/<suuid:room>', methods=['GET', 'POST'])
def hostRoomRedirect(room: UUID): def hostRoomRedirect(room: UUID):
return redirect(url_for("hostRoom", room=room)) return redirect(url_for("hostRoom", room=room))
@app.route('/favicon.ico') @app.route('/favicon.ico')
def favicon(): def favicon():
return send_from_directory(os.path.join(app.root_path, 'static/static'), return send_from_directory(os.path.join(app.root_path, 'static/static'),

View File

@ -1,26 +1,25 @@
import io
import itertools
import json import json
import logging
import os import os
import platform import platform
import struct import struct
import subprocess import subprocess
import random
import copy import copy
from Utils import local_path, is_frozen import threading
from .Utils import subprocess_args, data_path, get_version_bytes, __version__ from .Utils import subprocess_args, data_path, get_version_bytes, __version__
from .ntype import BigStream, uint32 from Utils import local_path
from .ntype import BigStream
from .crc import calculate_crc from .crc import calculate_crc
DMADATA_START = 0x7430 DMADATA_START = 0x7430
double_cache_prevention = threading.Lock()
class Rom(BigStream): class Rom(BigStream):
original = None
def __init__(self, file=None): def __init__(self, file=None):
super().__init__([]) super().__init__([])
self.original = None
self.changed_address = {} self.changed_address = {}
self.changed_dma = {} self.changed_dma = {}
self.force_patch = [] self.force_patch = []
@ -28,13 +27,11 @@ class Rom(BigStream):
if file is None: if file is None:
return return
decomp_file = 'ZOOTDEC.z64' decomp_file = local_path('ZOOTDEC.z64')
os.chdir(local_path())
with open(data_path('generated/symbols.json'), 'r') as stream: with open(data_path('generated/symbols.json'), 'r') as stream:
symbols = json.load(stream) symbols = json.load(stream)
self.symbols = { name: int(addr, 16) for name, addr in symbols.items() } self.symbols = {name: int(addr, 16) for name, addr in symbols.items()}
# If decompressed file already exists, read from it # If decompressed file already exists, read from it
if os.path.exists(decomp_file): if os.path.exists(decomp_file):
@ -56,13 +53,14 @@ class Rom(BigStream):
# Add file to maximum size # Add file to maximum size
self.buffer.extend(bytearray([0x00] * (0x4000000 - len(self.buffer)))) self.buffer.extend(bytearray([0x00] * (0x4000000 - len(self.buffer))))
self.original = self.copy() with double_cache_prevention:
if not self.original:
Rom.original = self.copy()
# Add version number to header. # Add version number to header.
self.write_bytes(0x35, get_version_bytes(__version__)) self.write_bytes(0x35, get_version_bytes(__version__))
self.force_patch.extend([0x35, 0x36, 0x37]) self.force_patch.extend([0x35, 0x36, 0x37])
def copy(self): def copy(self):
new_rom = Rom() new_rom = Rom()
new_rom.buffer = copy.copy(self.buffer) new_rom.buffer = copy.copy(self.buffer)
@ -71,7 +69,6 @@ class Rom(BigStream):
new_rom.force_patch = copy.copy(self.force_patch) new_rom.force_patch = copy.copy(self.force_patch)
return new_rom return new_rom
def decompress_rom_file(self, file, decomp_file): def decompress_rom_file(self, file, decomp_file):
validCRC = [ validCRC = [
[0xEC, 0x70, 0x11, 0xB7, 0x76, 0x16, 0xD7, 0x2B], # Compressed [0xEC, 0x70, 0x11, 0xB7, 0x76, 0x16, 0xD7, 0x2B], # Compressed
@ -85,7 +82,8 @@ class Rom(BigStream):
if romCRC not in validCRC: if romCRC not in validCRC:
# Bad CRC validation # Bad CRC validation
raise RuntimeError('ROM file %s is not a valid OoT 1.0 US ROM.' % file) raise RuntimeError('ROM file %s is not a valid OoT 1.0 US ROM.' % file)
elif len(self.buffer) < 0x2000000 or len(self.buffer) > (0x4000000) or file_name[1].lower() not in ['.z64', '.n64']: elif len(self.buffer) < 0x2000000 or len(self.buffer) > (0x4000000) or file_name[1].lower() not in ['.z64',
'.n64']:
# ROM is too big, or too small, or not a bad type # ROM is too big, or too small, or not a bad type
raise RuntimeError('ROM file %s is not a valid OoT 1.0 US ROM.' % file) raise RuntimeError('ROM file %s is not a valid OoT 1.0 US ROM.' % file)
elif len(self.buffer) == 0x2000000: elif len(self.buffer) == 0x2000000:
@ -107,7 +105,8 @@ class Rom(BigStream):
elif platform.system() == 'Darwin': elif platform.system() == 'Darwin':
subcall = [sub_dir + "/Decompress.out", file, decomp_file] subcall = [sub_dir + "/Decompress.out", file, decomp_file]
else: else:
raise RuntimeError('Unsupported operating system for decompression. Please supply an already decompressed ROM.') raise RuntimeError(
'Unsupported operating system for decompression. Please supply an already decompressed ROM.')
if not os.path.exists(subcall[0]): if not os.path.exists(subcall[0]):
raise RuntimeError(f'Decompressor does not exist! Please place it at {subcall[0]}.') raise RuntimeError(f'Decompressor does not exist! Please place it at {subcall[0]}.')
@ -117,16 +116,13 @@ class Rom(BigStream):
# ROM file is a valid and already uncompressed # ROM file is a valid and already uncompressed
pass pass
def write_byte(self, address, value): def write_byte(self, address, value):
super().write_byte(address, value) super().write_byte(address, value)
self.changed_address[self.last_address-1] = value self.changed_address[self.last_address - 1] = value
def write_bytes(self, address, values): def write_bytes(self, address, values):
super().write_bytes(address, values) super().write_bytes(address, values)
self.changed_address.update(zip(range(address, address+len(values)), values)) self.changed_address.update(zip(range(address, address + len(values)), values))
def restore(self): def restore(self):
self.buffer = copy.copy(self.original.buffer) self.buffer = copy.copy(self.original.buffer)
@ -137,23 +133,19 @@ class Rom(BigStream):
self.write_bytes(0x35, get_version_bytes(__version__)) self.write_bytes(0x35, get_version_bytes(__version__))
self.force_patch.extend([0x35, 0x36, 0x37]) self.force_patch.extend([0x35, 0x36, 0x37])
def sym(self, symbol_name): def sym(self, symbol_name):
return self.symbols.get(symbol_name) return self.symbols.get(symbol_name)
def write_to_file(self, file): def write_to_file(self, file):
self.verify_dmadata() self.verify_dmadata()
self.update_header() self.update_header()
with open(file, 'wb') as outfile: with open(file, 'wb') as outfile:
outfile.write(self.buffer) outfile.write(self.buffer)
def update_header(self): def update_header(self):
crc = calculate_crc(self) crc = calculate_crc(self)
self.write_bytes(0x10, crc) self.write_bytes(0x10, crc)
def read_rom(self, file): def read_rom(self, file):
# "Reads rom into bytearray" # "Reads rom into bytearray"
try: try:
@ -162,16 +154,14 @@ class Rom(BigStream):
except FileNotFoundError as ex: except FileNotFoundError as ex:
raise FileNotFoundError('Invalid path to Base ROM: "' + file + '"') raise FileNotFoundError('Invalid path to Base ROM: "' + file + '"')
# dmadata/file management helper functions # dmadata/file management helper functions
def _get_dmadata_record(self, cur): def _get_dmadata_record(self, cur):
start = self.read_int32(cur) start = self.read_int32(cur)
end = self.read_int32(cur+0x04) end = self.read_int32(cur + 0x04)
size = end-start size = end - start
return start, end, size return start, end, size
def get_dmadata_record_by_key(self, key): def get_dmadata_record_by_key(self, key):
cur = DMADATA_START cur = DMADATA_START
dma_start, dma_end, dma_size = self._get_dmadata_record(cur) dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
@ -183,7 +173,6 @@ class Rom(BigStream):
cur += 0x10 cur += 0x10
dma_start, dma_end, dma_size = self._get_dmadata_record(cur) dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
def verify_dmadata(self): def verify_dmadata(self):
cur = DMADATA_START cur = DMADATA_START
overlapping_records = [] overlapping_records = []
@ -214,7 +203,6 @@ class Rom(BigStream):
raise Exception("Overlapping DMA Data Records!\n%s" % \ raise Exception("Overlapping DMA Data Records!\n%s" % \
'\n-------------------------------------\n'.join(overlapping_records)) '\n-------------------------------------\n'.join(overlapping_records))
# update dmadata record with start vrom address "key" # update dmadata record with start vrom address "key"
# if key is not found, then attempt to add a new dmadata entry # if key is not found, then attempt to add a new dmadata entry
def update_dmadata_record(self, key, start, end, from_file=None): def update_dmadata_record(self, key, start, end, from_file=None):
@ -240,7 +228,6 @@ class Rom(BigStream):
from_file = key from_file = key
self.changed_dma[dma_index] = (from_file, start, end - start) self.changed_dma[dma_index] = (from_file, start, end - start)
def get_dma_table_range(self): def get_dma_table_range(self):
cur = DMADATA_START cur = DMADATA_START
dma_start, dma_end, dma_size = self._get_dmadata_record(cur) dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
@ -254,7 +241,6 @@ class Rom(BigStream):
cur += 0x10 cur += 0x10
dma_start, dma_end, dma_size = self._get_dmadata_record(cur) dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
# This will scan for any changes that have been made to the DMA table # This will scan for any changes that have been made to the DMA table
# This assumes any changes here are new files, so this should only be called # This assumes any changes here are new files, so this should only be called
# after patching in the new files, but before vanilla files are repointed # after patching in the new files, but before vanilla files are repointed
@ -279,7 +265,6 @@ class Rom(BigStream):
dma_start, dma_end, dma_size = self._get_dmadata_record(cur) dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
old_dma_start, old_dma_end, old_dma_size = self.original._get_dmadata_record(cur) old_dma_start, old_dma_end, old_dma_size = self.original._get_dmadata_record(cur)
# gets the last used byte of rom defined in the DMA table # gets the last used byte of rom defined in the DMA table
def free_space(self): def free_space(self):
cur = DMADATA_START cur = DMADATA_START
@ -296,6 +281,7 @@ class Rom(BigStream):
max_end = ((max_end + 0x0F) >> 4) << 4 max_end = ((max_end + 0x0F) >> 4) << 4
return max_end return max_end
def compress_rom_file(input_file, output_file): def compress_rom_file(input_file, output_file):
subcall = [] subcall = []

View File

@ -6,9 +6,11 @@ from functools import lru_cache
__version__ = Utils.__version__ + ' f.LUM' __version__ = Utils.__version__ + ' f.LUM'
def data_path(*args): def data_path(*args):
return Utils.local_path('worlds', 'oot', 'data', *args) return Utils.local_path('worlds', 'oot', 'data', *args)
@lru_cache(maxsize=13) # Cache Overworld.json and the 12 dungeons @lru_cache(maxsize=13) # Cache Overworld.json and the 12 dungeons
def read_json(file_path): def read_json(file_path):
json_string = "" json_string = ""
@ -20,11 +22,9 @@ def read_json(file_path):
return json.loads(json_string) return json.loads(json_string)
except json.JSONDecodeError as error: except json.JSONDecodeError as error:
raise Exception("JSON parse error around text:\n" + \ raise Exception("JSON parse error around text:\n" + \
json_string[error.pos-35:error.pos+35] + "\n" + \ json_string[error.pos - 35:error.pos + 35] + "\n" + \
" ^^\n") " ^^\n")
def is_bundled():
return getattr(sys, 'frozen', False)
# From the pyinstaller Wiki: https://github.com/pyinstaller/pyinstaller/wiki/Recipe-subprocess # From the pyinstaller Wiki: https://github.com/pyinstaller/pyinstaller/wiki/Recipe-subprocess
# Create a set of arguments which make a ``subprocess.Popen`` (and # Create a set of arguments which make a ``subprocess.Popen`` (and
@ -60,16 +60,17 @@ def subprocess_args(include_stdout=True):
ret.update({'stdin': subprocess.PIPE, ret.update({'stdin': subprocess.PIPE,
'stderr': subprocess.PIPE, 'stderr': subprocess.PIPE,
'startupinfo': si, 'startupinfo': si,
'env': env }) 'env': env})
return ret return ret
def get_version_bytes(a): def get_version_bytes(a):
version_bytes = [0x00, 0x00, 0x00] version_bytes = [0x00, 0x00, 0x00]
if not a: if not a:
return version_bytes; return version_bytes
sa = a.replace('v', '').replace(' ', '.').split('.') sa = a.replace('v', '').replace(' ', '.').split('.')
for i in range(0,3): for i in range(0, 3):
try: try:
version_byte = int(sa[i]) version_byte = int(sa[i])
except ValueError: except ValueError:
@ -78,6 +79,7 @@ def get_version_bytes(a):
return version_bytes return version_bytes
def compare_version(a, b): def compare_version(a, b):
if not a and not b: if not a and not b:
return 0 return 0
@ -89,7 +91,7 @@ def compare_version(a, b):
sa = get_version_bytes(a) sa = get_version_bytes(a)
sb = get_version_bytes(b) sb = get_version_bytes(b)
for i in range(0,3): for i in range(0, 3):
if sa[i] > sb[i]: if sa[i] > sb[i]:
return 1 return 1
if sa[i] < sb[i]: if sa[i] < sb[i]:

View File

@ -1,5 +1,5 @@
import logging import logging
import os import threading
import copy import copy
from collections import Counter from collections import Counter
@ -33,17 +33,21 @@ from ..AutoWorld import World
location_id_offset = 67000 location_id_offset = 67000
# OoT's generate_output doesn't benefit from more than 2 threads, instead it uses a lot of memory.
i_o_limiter = threading.Semaphore(2)
class OOTWorld(World): class OOTWorld(World):
game: str = "Ocarina of Time" game: str = "Ocarina of Time"
options: dict = oot_options options: dict = oot_options
topology_present: bool = True topology_present: bool = True
item_name_to_id = {item_name: oot_data_to_ap_id(data, False) for item_name, data in item_table.items() if data[2] is not None} item_name_to_id = {item_name: oot_data_to_ap_id(data, False) for item_name, data in item_table.items() if
data[2] is not None}
location_name_to_id = location_name_to_id location_name_to_id = location_name_to_id
remote_items: bool = False remote_items: bool = False
data_version = 1 data_version = 1
def __new__(cls, world, player): def __new__(cls, world, player):
# Add necessary objects to CollectionState on initialization # Add necessary objects to CollectionState on initialization
orig_init = CollectionState.__init__ orig_init = CollectionState.__init__
@ -81,11 +85,11 @@ class OOTWorld(World):
return super().__new__(cls) return super().__new__(cls)
def generate_early(self): def generate_early(self):
# Player name MUST be at most 16 bytes ascii-encoded, otherwise won't write to ROM correctly # Player name MUST be at most 16 bytes ascii-encoded, otherwise won't write to ROM correctly
if len(bytes(self.world.get_player_name(self.player), 'ascii')) > 16: if len(bytes(self.world.get_player_name(self.player), 'ascii')) > 16:
raise Exception(f"OoT: Player {self.player}'s name ({self.world.get_player_name(self.player)}) must be ASCII-compatible") raise Exception(
f"OoT: Player {self.player}'s name ({self.world.get_player_name(self.player)}) must be ASCII-compatible")
self.parser = Rule_AST_Transformer(self, self.player) self.parser = Rule_AST_Transformer(self, self.player)
@ -109,12 +113,16 @@ class OOTWorld(World):
self.file_hash = [self.world.random.randint(0, 31) for i in range(5)] self.file_hash = [self.world.random.randint(0, 31) for i in range(5)]
self.item_name_groups = { self.item_name_groups = {
"medallions": {"Light Medallion", "Forest Medallion", "Fire Medallion", "Water Medallion", "Shadow Medallion", "Spirit Medallion"}, "medallions": {"Light Medallion", "Forest Medallion", "Fire Medallion", "Water Medallion",
"Shadow Medallion", "Spirit Medallion"},
"stones": {"Kokiri Emerald", "Goron Ruby", "Zora Sapphire"}, "stones": {"Kokiri Emerald", "Goron Ruby", "Zora Sapphire"},
"rewards": {"Light Medallion", "Forest Medallion", "Fire Medallion", "Water Medallion", "Shadow Medallion", "Spirit Medallion", \ "rewards": {"Light Medallion", "Forest Medallion", "Fire Medallion", "Water Medallion", "Shadow Medallion",
"Spirit Medallion", \
"Kokiri Emerald", "Goron Ruby", "Zora Sapphire"}, "Kokiri Emerald", "Goron Ruby", "Zora Sapphire"},
"bottles": {"Bottle", "Bottle with Milk", "Deliver Letter", "Sell Big Poe", "Bottle with Red Potion", "Bottle with Green Potion", \ "bottles": {"Bottle", "Bottle with Milk", "Deliver Letter", "Sell Big Poe", "Bottle with Red Potion",
"Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish", "Bottle with Blue Fire", "Bottle with Bugs", "Bottle with Poe"} "Bottle with Green Potion", \
"Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish", "Bottle with Blue Fire",
"Bottle with Bugs", "Bottle with Poe"}
} }
# Incompatible option handling # Incompatible option handling
@ -171,7 +179,8 @@ class OOTWorld(World):
self.spawn_positions = False self.spawn_positions = False
# Set internal names used by the OoT generator # Set internal names used by the OoT generator
self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld'] # only 'keysanity' and 'remove' implemented self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon',
'overworld'] # only 'keysanity' and 'remove' implemented
# Hint stuff # Hint stuff
self.misc_hints = True # this is just always on self.misc_hints = True # this is just always on
@ -193,7 +202,6 @@ class OOTWorld(World):
self.shopsanity = self.shopsanity.replace('_value', '') # can't set "random" manually self.shopsanity = self.shopsanity.replace('_value', '') # can't set "random" manually
self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '') self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '')
# Get hint distribution # Get hint distribution
self.hint_dist_user = read_json(data_path('Hints', f'{self.hint_dist}.json')) self.hint_dist_user = read_json(data_path('Hints', f'{self.hint_dist}.json'))
@ -230,8 +238,6 @@ class OOTWorld(World):
self.always_hints = [hint.name for hint in getRequiredHints(self)] self.always_hints = [hint.name for hint in getRequiredHints(self)]
def load_regions_from_json(self, file_path): def load_regions_from_json(self, file_path):
region_json = read_json(file_path) region_json = read_json(file_path)
@ -294,7 +300,6 @@ class OOTWorld(World):
self.regions.append(new_region) self.regions.append(new_region)
self.world._recache() self.world._recache()
def set_scrub_prices(self): def set_scrub_prices(self):
# Get Deku Scrub Locations # Get Deku Scrub Locations
scrub_locations = [location for location in self.get_locations() if 'Deku Scrub' in location.name] scrub_locations = [location for location in self.get_locations() if 'Deku Scrub' in location.name]
@ -323,13 +328,12 @@ class OOTWorld(World):
if location.item is not None: if location.item is not None:
location.item.price = price location.item.price = price
def random_shop_prices(self): def random_shop_prices(self):
shop_item_indexes = ['7', '5', '8', '6'] shop_item_indexes = ['7', '5', '8', '6']
self.shop_prices = {} self.shop_prices = {}
for region in self.regions: for region in self.regions:
if self.shopsanity == 'random': if self.shopsanity == 'random':
shop_item_count = self.world.random.randint(0,4) shop_item_count = self.world.random.randint(0, 4)
else: else:
shop_item_count = int(self.shopsanity) shop_item_count = int(self.shopsanity)
@ -338,7 +342,6 @@ class OOTWorld(World):
if location.name[-1:] in shop_item_indexes[:shop_item_count]: if location.name[-1:] in shop_item_indexes[:shop_item_count]:
self.shop_prices[location.name] = int(self.world.random.betavariate(1.5, 2) * 60) * 5 self.shop_prices[location.name] = int(self.world.random.betavariate(1.5, 2) * 60) * 5
def fill_bosses(self, bossCount=9): def fill_bosses(self, bossCount=9):
rewardlist = ( rewardlist = (
'Kokiri Emerald', 'Kokiri Emerald',
@ -381,13 +384,11 @@ class OOTWorld(World):
loc.locked = True loc.locked = True
loc.event = True loc.event = True
def create_item(self, name: str): def create_item(self, name: str):
if name in item_table: if name in item_table:
return OOTItem(name, self.player, item_table[name], False) return OOTItem(name, self.player, item_table[name], False)
return OOTItem(name, self.player, ('Event', True, None, None), True) return OOTItem(name, self.player, ('Event', True, None, None), True)
def make_event_item(self, name, location, item=None): def make_event_item(self, name, location, item=None):
if item is None: if item is None:
item = self.create_item(name) item = self.create_item(name)
@ -398,7 +399,6 @@ class OOTWorld(World):
location.internal = True location.internal = True
return item return item
def create_regions(self): # create and link regions def create_regions(self): # create and link regions
if self.logic_rules == 'glitchless': if self.logic_rules == 'glitchless':
world_type = 'World' world_type = 'World'
@ -427,11 +427,9 @@ class OOTWorld(World):
if self.entrance_shuffle: if self.entrance_shuffle:
shuffle_random_entrances(self) shuffle_random_entrances(self)
def set_rules(self): def set_rules(self):
set_rules(self) set_rules(self)
def generate_basic(self): # generate item pools, place fixed items def generate_basic(self): # generate item pools, place fixed items
# Generate itempool # Generate itempool
generate_itempool(self) generate_itempool(self)
@ -500,22 +498,28 @@ class OOTWorld(World):
# We can't put a dungeon item on the end of a dungeon if a song is supposed to go there. Make sure not to include it. # We can't put a dungeon item on the end of a dungeon if a song is supposed to go there. Make sure not to include it.
dungeon_locations = [loc for region in dungeon.regions for loc in region.locations dungeon_locations = [loc for region in dungeon.regions for loc in region.locations
if loc.item is None and (self.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations)] if loc.item is None and (
self.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations)]
if itempools['dungeon']: # only do this if there's anything to shuffle if itempools['dungeon']: # only do this if there's anything to shuffle
self.world.random.shuffle(dungeon_locations) self.world.random.shuffle(dungeon_locations)
fill_restrictive(self.world, self.state_with_items(self.itempool), dungeon_locations, itempools['dungeon'], True, True) fill_restrictive(self.world, self.state_with_items(self.itempool), dungeon_locations,
itempools['dungeon'], True, True)
any_dungeon_locations.extend(dungeon_locations) # adds only the unfilled locations any_dungeon_locations.extend(dungeon_locations) # adds only the unfilled locations
# Now fill items that can go into any dungeon. Retrieve the Gerudo Fortress keys from the pool if necessary # Now fill items that can go into any dungeon. Retrieve the Gerudo Fortress keys from the pool if necessary
if self.shuffle_fortresskeys == 'any_dungeon': if self.shuffle_fortresskeys == 'any_dungeon':
fortresskeys = list(filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey', self.itempool)) fortresskeys = list(
filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey', self.itempool))
itempools['any_dungeon'].extend(fortresskeys) itempools['any_dungeon'].extend(fortresskeys)
for key in fortresskeys: for key in fortresskeys:
self.itempool.remove(key) self.itempool.remove(key)
if itempools['any_dungeon']: if itempools['any_dungeon']:
itempools['any_dungeon'].sort(key=lambda item: {'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0)) itempools['any_dungeon'].sort(
key=lambda item: {'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type,
0))
self.world.random.shuffle(any_dungeon_locations) self.world.random.shuffle(any_dungeon_locations)
fill_restrictive(self.world, self.state_with_items(self.itempool), any_dungeon_locations, itempools['any_dungeon'], True, True) fill_restrictive(self.world, self.state_with_items(self.itempool), any_dungeon_locations,
itempools['any_dungeon'], True, True)
# If anything is overworld-only, enforce them as local and not in the remaining dungeon locations # If anything is overworld-only, enforce them as local and not in the remaining dungeon locations
if itempools['overworld'] or self.shuffle_fortresskeys == 'overworld': if itempools['overworld'] or self.shuffle_fortresskeys == 'overworld':
@ -553,8 +557,9 @@ class OOTWorld(World):
try: try:
self.world.random.shuffle(songs) # shuffling songs makes it less likely to fail by placing ZL last self.world.random.shuffle(songs) # shuffling songs makes it less likely to fail by placing ZL last
self.world.random.shuffle(song_locations) self.world.random.shuffle(song_locations)
fill_restrictive(self.world, self.state_with_items(self.itempool), song_locations[:], songs[:], True, True) fill_restrictive(self.world, self.state_with_items(self.itempool), song_locations[:], songs[:],
logger.debug(f"Successfully placed songs for player {self.player} after {6-tries} attempt(s)") True, True)
logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)")
tries = 0 tries = 0
except FillError as e: except FillError as e:
tries -= 1 tries -= 1
@ -574,7 +579,8 @@ class OOTWorld(World):
# fast fill will fail because there is some logic on the shop items. we'll gather them up and place the shop items # fast fill will fail because there is some logic on the shop items. we'll gather them up and place the shop items
if self.shopsanity != 'off': if self.shopsanity != 'off':
shop_items = list(filter(lambda item: item.player == self.player and item.type == 'Shop', self.itempool)) shop_items = list(filter(lambda item: item.player == self.player and item.type == 'Shop', self.itempool))
shop_locations = list(filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices, shop_locations = list(
filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
self.world.get_unfilled_locations(player=self.player))) self.world.get_unfilled_locations(player=self.player)))
shop_items.sort(key=lambda item: 1 if item.name in ["Buy Goron Tunic", "Buy Zora Tunic"] else 0) shop_items.sort(key=lambda item: 1 if item.name in ["Buy Goron Tunic", "Buy Zora Tunic"] else 0)
self.world.random.shuffle(shop_locations) self.world.random.shuffle(shop_locations)
@ -586,7 +592,8 @@ class OOTWorld(World):
# Locations which are not sendable must be converted to events # Locations which are not sendable must be converted to events
# This includes all locations for which show_in_spoiler is false, and shuffled shop items. # This includes all locations for which show_in_spoiler is false, and shuffled shop items.
for loc in self.get_locations(): for loc in self.get_locations():
if loc.address is not None and (not loc.show_in_spoiler or (loc.item is not None and loc.item.type == 'Shop') if loc.address is not None and (
not loc.show_in_spoiler or (loc.item is not None and loc.item.type == 'Shop')
or (self.skip_child_zelda and loc.name in ['HC Zeldas Letter', 'Song from Impa'])): or (self.skip_child_zelda and loc.name in ['HC Zeldas Letter', 'Song from Impa'])):
loc.address = None loc.address = None
@ -595,7 +602,8 @@ class OOTWorld(World):
if self.ice_trap_appearance in ['major_only', 'anything']: if self.ice_trap_appearance in ['major_only', 'anything']:
self.fake_items.extend([item for item in self.itempool if item.index and self.is_major_item(item)]) self.fake_items.extend([item for item in self.itempool if item.index and self.is_major_item(item)])
if self.ice_trap_appearance in ['junk_only', 'anything']: if self.ice_trap_appearance in ['junk_only', 'anything']:
self.fake_items.extend([item for item in self.itempool if item.index and not self.is_major_item(item) and item.name != 'Ice Trap']) self.fake_items.extend([item for item in self.itempool if
item.index and not self.is_major_item(item) and item.name != 'Ice Trap'])
# Put all remaining items into the general itempool # Put all remaining items into the general itempool
self.world.itempool += self.itempool self.world.itempool += self.itempool
@ -605,7 +613,8 @@ class OOTWorld(World):
all_state = self.state_with_items(self.itempool) all_state = self.state_with_items(self.itempool)
all_locations = [loc for loc in self.world.get_locations() if loc.player == self.player] all_locations = [loc for loc in self.world.get_locations() if loc.player == self.player]
reachable = self.world.get_reachable_locations(all_state, self.player) reachable = self.world.get_reachable_locations(all_state, self.player)
unreachable = [loc for loc in all_locations if loc.internal and loc.event and loc.locked and loc not in reachable] unreachable = [loc for loc in all_locations if
loc.internal and loc.event and loc.locked and loc not in reachable]
for loc in unreachable: for loc in unreachable:
loc.parent_region.locations.remove(loc) loc.parent_region.locations.remove(loc)
# Exception: Sell Big Poe is an event which is only reachable if Bottle with Big Poe is in the item pool. # Exception: Sell Big Poe is an event which is only reachable if Bottle with Big Poe is in the item pool.
@ -630,7 +639,8 @@ class OOTWorld(World):
if self.skip_child_zelda and impa.item is None: if self.skip_child_zelda and impa.item is None:
from .SaveContext import SaveContext from .SaveContext import SaveContext
item_to_place = self.world.random.choice([item for item in self.world.itempool item_to_place = self.world.random.choice([item for item in self.world.itempool
if item.player == self.player and item.name in SaveContext.giveable_items]) if
item.player == self.player and item.name in SaveContext.giveable_items])
self.world.push_item(impa, item_to_place, False) self.world.push_item(impa, item_to_place, False)
impa.locked = True impa.locked = True
impa.event = True impa.event = True
@ -638,22 +648,23 @@ class OOTWorld(World):
# For now we will always output a patch file. # For now we will always output a patch file.
def generate_output(self, output_directory: str): def generate_output(self, output_directory: str):
with i_o_limiter:
# Make ice traps appear as other random items # Make ice traps appear as other random items
ice_traps = [loc.item for loc in self.get_locations() if loc.item.name == 'Ice Trap'] ice_traps = [loc.item for loc in self.get_locations() if loc.item.name == 'Ice Trap']
for trap in ice_traps: for trap in ice_traps:
trap.looks_like_item = self.create_item(self.world.slot_seeds[self.player].choice(self.fake_items).name) trap.looks_like_item = self.create_item(self.world.slot_seeds[self.player].choice(self.fake_items).name)
outfile_name = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_player_name(self.player)}" outfile_name = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_player_name(self.player)}"
rom = Rom(file=get_options()['oot_options']['rom_file']) # a ROM must be provided, cannot produce patches without it rom = Rom(
file=get_options()['oot_options']['rom_file']) # a ROM must be provided, cannot produce patches without it
if self.hints != 'none': if self.hints != 'none':
buildWorldGossipHints(self) buildWorldGossipHints(self)
patch_rom(self, rom) patch_rom(self, rom)
patch_cosmetics(self, rom) patch_cosmetics(self, rom)
rom.update_header() rom.update_header()
create_patch_file(rom, output_path(output_directory, outfile_name+'.apz5')) create_patch_file(rom, output_path(output_directory, outfile_name + '.apz5'))
rom.restore() rom.restore()
# Helper functions # Helper functions
def get_shuffled_entrances(self): def get_shuffled_entrances(self):
return [] return []
@ -698,7 +709,6 @@ class OOTWorld(World):
return True return True
# Run this once for to gather up all required locations (for WOTH), barren regions (for foolish), and location of major items. # Run this once for to gather up all required locations (for WOTH), barren regions (for foolish), and location of major items.
# required_locations and major_item_locations need to be ordered for deterministic hints. # required_locations and major_item_locations need to be ordered for deterministic hints.
def gather_hint_data(self): def gather_hint_data(self):
@ -710,8 +720,8 @@ class OOTWorld(World):
items_by_region[r.hint_text] = {'dungeon': False, 'weight': 0, 'prog_items': 0} items_by_region[r.hint_text] = {'dungeon': False, 'weight': 0, 'prog_items': 0}
for d in self.dungeons: for d in self.dungeons:
items_by_region[d.hint_text] = {'dungeon': True, 'weight': 0, 'prog_items': 0} items_by_region[d.hint_text] = {'dungeon': True, 'weight': 0, 'prog_items': 0}
del(items_by_region["Link's Pocket"]) del (items_by_region["Link's Pocket"])
del(items_by_region[None]) del (items_by_region[None])
for loc in self.get_locations(): for loc in self.get_locations():
if loc.item.code: # is a real item if loc.item.code: # is a real item