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 get_seed, seeddigits
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.Text import TextTable
from worlds.alttp.Regions import location_table, key_drop_data
@ -735,4 +735,4 @@ if __name__ == '__main__':
confirmation = atexit.register(input, "Press enter to close.")
main()
# in case of error-free exit should not need confirmation
atexit.unregister(confirmation)
atexit.unregister(confirmation)

View File

@ -82,7 +82,6 @@ def page_not_found(err):
return render_template('404.html'), 404
# Player settings pages
@app.route('/games/<string:game>/player-settings')
def player_settings(game):
@ -176,10 +175,12 @@ def hostRoom(room: UUID):
return render_template("hostRoom.html", room=room)
@app.route('/hosted/<suuid:room>', methods=['GET', 'POST'])
def hostRoomRedirect(room: UUID):
return redirect(url_for("hostRoom", room=room))
@app.route('/favicon.ico')
def favicon():
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 logging
import os
import platform
import struct
import subprocess
import random
import copy
from Utils import local_path, is_frozen
import threading
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
DMADATA_START = 0x7430
double_cache_prevention = threading.Lock()
class Rom(BigStream):
original = None
def __init__(self, file=None):
super().__init__([])
self.original = None
self.changed_address = {}
self.changed_dma = {}
self.force_patch = []
@ -28,13 +27,11 @@ class Rom(BigStream):
if file is None:
return
decomp_file = 'ZOOTDEC.z64'
os.chdir(local_path())
decomp_file = local_path('ZOOTDEC.z64')
with open(data_path('generated/symbols.json'), 'r') as 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 os.path.exists(decomp_file):
@ -56,13 +53,14 @@ class Rom(BigStream):
# Add file to maximum size
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.
self.write_bytes(0x35, get_version_bytes(__version__))
self.force_patch.extend([0x35, 0x36, 0x37])
def copy(self):
new_rom = Rom()
new_rom.buffer = copy.copy(self.buffer)
@ -71,12 +69,11 @@ class Rom(BigStream):
new_rom.force_patch = copy.copy(self.force_patch)
return new_rom
def decompress_rom_file(self, file, decomp_file):
validCRC = [
[0xEC, 0x70, 0x11, 0xB7, 0x76, 0x16, 0xD7, 0x2B], # Compressed
[0x70, 0xEC, 0xB7, 0x11, 0x16, 0x76, 0x2B, 0xD7], # Byteswap compressed
[0x93, 0x52, 0x2E, 0x7B, 0xE5, 0x06, 0xD4, 0x27], # Decompressed
[0xEC, 0x70, 0x11, 0xB7, 0x76, 0x16, 0xD7, 0x2B], # Compressed
[0x70, 0xEC, 0xB7, 0x11, 0x16, 0x76, 0x2B, 0xD7], # Byteswap compressed
[0x93, 0x52, 0x2E, 0x7B, 0xE5, 0x06, 0xD4, 0x27], # Decompressed
]
# Validate ROM file
@ -85,7 +82,8 @@ class Rom(BigStream):
if romCRC not in validCRC:
# Bad CRC validation
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
raise RuntimeError('ROM file %s is not a valid OoT 1.0 US ROM.' % file)
elif len(self.buffer) == 0x2000000:
@ -107,7 +105,8 @@ class Rom(BigStream):
elif platform.system() == 'Darwin':
subcall = [sub_dir + "/Decompress.out", file, decomp_file]
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]):
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
pass
def write_byte(self, 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):
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):
self.buffer = copy.copy(self.original.buffer)
@ -137,23 +133,19 @@ class Rom(BigStream):
self.write_bytes(0x35, get_version_bytes(__version__))
self.force_patch.extend([0x35, 0x36, 0x37])
def sym(self, symbol_name):
return self.symbols.get(symbol_name)
def write_to_file(self, file):
self.verify_dmadata()
self.update_header()
with open(file, 'wb') as outfile:
outfile.write(self.buffer)
def update_header(self):
crc = calculate_crc(self)
self.write_bytes(0x10, crc)
def read_rom(self, file):
# "Reads rom into bytearray"
try:
@ -162,16 +154,14 @@ class Rom(BigStream):
except FileNotFoundError as ex:
raise FileNotFoundError('Invalid path to Base ROM: "' + file + '"')
# dmadata/file management helper functions
def _get_dmadata_record(self, cur):
start = self.read_int32(cur)
end = self.read_int32(cur+0x04)
size = end-start
end = self.read_int32(cur + 0x04)
size = end - start
return start, end, size
def get_dmadata_record_by_key(self, key):
cur = DMADATA_START
dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
@ -183,7 +173,6 @@ class Rom(BigStream):
cur += 0x10
dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
def verify_dmadata(self):
cur = DMADATA_START
overlapping_records = []
@ -206,14 +195,13 @@ class Rom(BigStream):
if this_end > next_start:
overlapping_records.append(
'0x%08X - 0x%08X (Size: 0x%04X)\n0x%08X - 0x%08X (Size: 0x%04X)' % \
(this_start, this_end, this_size, next_start, next_end, next_size)
)
'0x%08X - 0x%08X (Size: 0x%04X)\n0x%08X - 0x%08X (Size: 0x%04X)' % \
(this_start, this_end, this_size, next_start, next_end, next_size)
)
if len(overlapping_records) > 0:
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"
# if key is not found, then attempt to add a new dmadata entry
@ -240,7 +228,6 @@ class Rom(BigStream):
from_file = key
self.changed_dma[dma_index] = (from_file, start, end - start)
def get_dma_table_range(self):
cur = DMADATA_START
dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
@ -254,7 +241,6 @@ class Rom(BigStream):
cur += 0x10
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 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
@ -267,7 +253,7 @@ class Rom(BigStream):
while True:
if (dma_start == 0 and dma_end == 0) and \
(old_dma_start == 0 and old_dma_end == 0):
(old_dma_start == 0 and old_dma_end == 0):
break
# If the entries do not match, the flag the changed entry
@ -279,7 +265,6 @@ class Rom(BigStream):
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)
# gets the last used byte of rom defined in the DMA table
def free_space(self):
cur = DMADATA_START
@ -296,6 +281,7 @@ class Rom(BigStream):
max_end = ((max_end + 0x0F) >> 4) << 4
return max_end
def compress_rom_file(input_file, output_file):
subcall = []

View File

@ -6,9 +6,11 @@ from functools import lru_cache
__version__ = Utils.__version__ + ' f.LUM'
def data_path(*args):
def data_path(*args):
return Utils.local_path('worlds', 'oot', 'data', *args)
@lru_cache(maxsize=13) # Cache Overworld.json and the 12 dungeons
def read_json(file_path):
json_string = ""
@ -20,11 +22,9 @@ def read_json(file_path):
return json.loads(json_string)
except json.JSONDecodeError as error:
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")
def is_bundled():
return getattr(sys, 'frozen', False)
# From the pyinstaller Wiki: https://github.com/pyinstaller/pyinstaller/wiki/Recipe-subprocess
# 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,
'stderr': subprocess.PIPE,
'startupinfo': si,
'env': env })
'env': env})
return ret
def get_version_bytes(a):
version_bytes = [0x00, 0x00, 0x00]
if not a:
return version_bytes;
return version_bytes
sa = a.replace('v', '').replace(' ', '.').split('.')
for i in range(0,3):
for i in range(0, 3):
try:
version_byte = int(sa[i])
except ValueError:
@ -78,6 +79,7 @@ def get_version_bytes(a):
return version_bytes
def compare_version(a, b):
if not a and not b:
return 0
@ -89,7 +91,7 @@ def compare_version(a, b):
sa = get_version_bytes(a)
sb = get_version_bytes(b)
for i in range(0,3):
for i in range(0, 3):
if sa[i] > sb[i]:
return 1
if sa[i] < sb[i]:

View File

@ -1,5 +1,5 @@
import logging
import os
import threading
import copy
from collections import Counter
@ -33,17 +33,21 @@ from ..AutoWorld import World
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):
game: str = "Ocarina of Time"
options: dict = oot_options
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
remote_items: bool = False
data_version = 1
def __new__(cls, world, player):
# Add necessary objects to CollectionState on initialization
orig_init = CollectionState.__init__
@ -64,9 +68,9 @@ class OOTWorld(World):
ret.adult_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
range(1, self.world.players + 1)}
ret.child_blocked_connections = {player: copy.copy(self.child_blocked_connections[player]) for player in
range(1, self.world.players + 1)}
range(1, self.world.players + 1)}
ret.adult_blocked_connections = {player: copy.copy(self.adult_blocked_connections[player]) for player in
range(1, self.world.players + 1)}
range(1, self.world.players + 1)}
return ret
CollectionState.__init__ = oot_init
@ -81,17 +85,17 @@ class OOTWorld(World):
return super().__new__(cls)
def generate_early(self):
# 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:
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)
for (option_name, option) in oot_options.items():
for (option_name, option) in oot_options.items():
result = getattr(self.world, option_name)[self.player]
if isinstance(result, Range):
if isinstance(result, Range):
option_value = int(result)
elif isinstance(result, Toggle):
option_value = bool(result)
@ -109,17 +113,21 @@ class OOTWorld(World):
self.file_hash = [self.world.random.randint(0, 31) for i in range(5)]
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"},
"rewards": {"Light Medallion", "Forest Medallion", "Fire Medallion", "Water Medallion", "Shadow Medallion", "Spirit Medallion", \
"Kokiri Emerald", "Goron Ruby", "Zora Sapphire"},
"bottles": {"Bottle", "Bottle with Milk", "Deliver Letter", "Sell Big Poe", "Bottle with Red Potion", "Bottle with Green Potion", \
"Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish", "Bottle with Blue Fire", "Bottle with Bugs", "Bottle with Poe"}
"rewards": {"Light Medallion", "Forest Medallion", "Fire Medallion", "Water Medallion", "Shadow Medallion",
"Spirit Medallion", \
"Kokiri Emerald", "Goron Ruby", "Zora Sapphire"},
"bottles": {"Bottle", "Bottle with Milk", "Deliver Letter", "Sell Big Poe", "Bottle with Red Potion",
"Bottle with Green Potion", \
"Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish", "Bottle with Blue Fire",
"Bottle with Bugs", "Bottle with Poe"}
}
# Incompatible option handling
# ER and glitched logic are not compatible; glitched takes priority
if self.logic_rules == 'glitched':
if self.logic_rules == 'glitched':
self.shuffle_interior_entrances = False
self.shuffle_grotto_entrances = False
self.shuffle_dungeon_entrances = False
@ -129,7 +137,7 @@ class OOTWorld(World):
self.spawn_positions = False
# Closed forest and adult start are not compatible; closed forest takes priority
if self.open_forest == 'closed':
if self.open_forest == 'closed':
self.starting_age = 'child'
# Skip child zelda and shuffle egg are not compatible; skip-zelda takes priority
@ -149,16 +157,16 @@ class OOTWorld(World):
self.dungeon_mq = {item['name']: (item in mq_dungeons) for item in dungeon_table}
# Determine tricks in logic
for trick in self.logic_tricks:
for trick in self.logic_tricks:
normalized_name = trick.casefold()
if normalized_name in normalized_name_tricks:
if normalized_name in normalized_name_tricks:
setattr(self, normalized_name_tricks[normalized_name]['name'], True)
else:
raise Exception(f'Unknown OOT logic trick for player {self.player}: {trick}')
# Not implemented for now, but needed to placate the generator. Remove as they are implemented
self.mq_dungeons_random = False # this will be a deprecated option later
self.ocarina_songs = False # just need to pull in the OcarinaSongs module
self.ocarina_songs = False # just need to pull in the OcarinaSongs module
self.big_poe_count = 1 # disabled due to client-side issues for now
self.correct_chest_sizes = False # will probably never be implemented since multiworld items are always major
# ER options
@ -171,7 +179,8 @@ class OOTWorld(World):
self.spawn_positions = False
# 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
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.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '')
# Get hint distribution
self.hint_dist_user = read_json(data_path('Hints', f'{self.hint_dist}.json'))
@ -230,11 +238,9 @@ class OOTWorld(World):
self.always_hints = [hint.name for hint in getRequiredHints(self)]
def load_regions_from_json(self, file_path):
region_json = read_json(file_path)
for region in region_json:
new_region = OOTRegion(region['region_name'], RegionType.Generic, None, self.player)
new_region.world = self.world
@ -294,7 +300,6 @@ class OOTWorld(World):
self.regions.append(new_region)
self.world._recache()
def set_scrub_prices(self):
# Get Deku Scrub Locations
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:
location.item.price = price
def random_shop_prices(self):
shop_item_indexes = ['7', '5', '8', '6']
self.shop_prices = {}
for region in self.regions:
if self.shopsanity == 'random':
shop_item_count = self.world.random.randint(0,4)
shop_item_count = self.world.random.randint(0, 4)
else:
shop_item_count = int(self.shopsanity)
@ -338,8 +342,7 @@ class OOTWorld(World):
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
def fill_bosses(self, bossCount=9):
def fill_bosses(self, bossCount=9):
rewardlist = (
'Kokiri Emerald',
'Goron Ruby',
@ -381,13 +384,11 @@ class OOTWorld(World):
loc.locked = True
loc.event = True
def create_item(self, name: str):
if name in item_table:
def create_item(self, name: str):
if name in item_table:
return OOTItem(name, self.player, item_table[name], False)
return OOTItem(name, self.player, ('Event', True, None, None), True)
def make_event_item(self, name, location, item=None):
if item is None:
item = self.create_item(name)
@ -398,7 +399,6 @@ class OOTWorld(World):
location.internal = True
return item
def create_regions(self): # create and link regions
if self.logic_rules == 'glitchless':
world_type = 'World'
@ -427,33 +427,31 @@ class OOTWorld(World):
if self.entrance_shuffle:
shuffle_random_entrances(self)
def set_rules(self):
def set_rules(self):
set_rules(self)
def generate_basic(self): # generate item pools, place fixed items
# Generate itempool
generate_itempool(self)
junk_pool = get_junk_pool(self)
# Determine starting items
for item in self.world.precollected_items:
for item in self.world.precollected_items:
if item.player != self.player:
continue
if item.name in self.remove_from_start_inventory:
self.remove_from_start_inventory.remove(item.name)
else:
self.starting_items[item.name] += 1
if item.type == 'Song':
if item.type == 'Song':
self.starting_songs = True
# Call the junk fill and get a replacement
if item in self.itempool:
self.itempool.remove(item)
self.itempool.append(self.create_item(*get_junk_item(pool=junk_pool)))
if self.start_with_consumables:
if self.start_with_consumables:
self.starting_items['Deku Sticks'] = 30
self.starting_items['Deku Nuts'] = 40
if self.start_with_rupees:
if self.start_with_rupees:
self.starting_items['Rupees'] = 999
# Uniquely rename drop locations for each region and erase them from the spoiler
@ -486,7 +484,7 @@ class OOTWorld(World):
'keysanity': [],
}
any_dungeon_locations = []
for dungeon in self.dungeons:
for dungeon in self.dungeons:
itempools['dungeon'] = []
# Put the dungeon items into their appropriate pools.
# Build in reverse order since we need to fill boss key first and pop() returns the last element
@ -499,23 +497,29 @@ class OOTWorld(World):
itempools[shufflebk].extend(dungeon.boss_key)
# 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
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
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 itempools['dungeon']: # only do this if there's anything to shuffle
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
# 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':
fortresskeys = list(filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey', self.itempool))
if self.shuffle_fortresskeys == 'any_dungeon':
fortresskeys = list(
filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey', self.itempool))
itempools['any_dungeon'].extend(fortresskeys)
for key in fortresskeys:
self.itempool.remove(key)
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)
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 itempools['overworld'] or self.shuffle_fortresskeys == 'overworld':
@ -535,26 +539,27 @@ class OOTWorld(World):
# Place songs
# 5 built-in retries because this section can fail sometimes
if self.shuffle_song_items != 'any':
if self.shuffle_song_items != 'any':
tries = 5
if self.shuffle_song_items == 'song':
song_locations = list(filter(lambda location: location.type == 'Song',
self.world.get_unfilled_locations(player=self.player)))
elif self.shuffle_song_items == 'dungeon':
song_locations = list(filter(lambda location: location.name in dungeon_song_locations,
self.world.get_unfilled_locations(player=self.player)))
song_locations = list(filter(lambda location: location.type == 'Song',
self.world.get_unfilled_locations(player=self.player)))
elif self.shuffle_song_items == 'dungeon':
song_locations = list(filter(lambda location: location.name in dungeon_song_locations,
self.world.get_unfilled_locations(player=self.player)))
else:
raise Exception(f"Unknown song shuffle type: {self.shuffle_song_items}")
songs = list(filter(lambda item: item.player == self.player and item.type == 'Song', self.itempool))
for song in songs:
for song in songs:
self.itempool.remove(song)
while tries:
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)
fill_restrictive(self.world, self.state_with_items(self.itempool), song_locations[:], songs[:], True, True)
logger.debug(f"Successfully placed songs for player {self.player} after {6-tries} attempt(s)")
fill_restrictive(self.world, self.state_with_items(self.itempool), song_locations[:], songs[:],
True, True)
logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)")
tries = 0
except FillError as e:
tries -= 1
@ -572,13 +577,14 @@ class OOTWorld(World):
# Place 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_locations = list(filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
self.world.get_unfilled_locations(player=self.player)))
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)))
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)
for item in shop_items:
for item in shop_items:
self.itempool.remove(item)
fill_restrictive(self.world, self.state_with_items(self.itempool), shop_locations, shop_items, True, True)
set_shop_rules(self)
@ -586,8 +592,9 @@ class OOTWorld(World):
# 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.
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')
or (self.skip_child_zelda and loc.name in ['HC Zeldas Letter', 'Song from Impa'])):
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'])):
loc.address = None
# Gather items for ice trap appearances
@ -595,7 +602,8 @@ class OOTWorld(World):
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)])
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
self.world.itempool += self.itempool
@ -605,8 +613,9 @@ class OOTWorld(World):
all_state = self.state_with_items(self.itempool)
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)
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:
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:
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.
# We allow it to be removed only if Bottle with Big Poe is not in the itempool.
@ -629,30 +638,32 @@ class OOTWorld(World):
impa = self.world.get_location("Song from Impa", self.player)
if self.skip_child_zelda and impa.item is None:
from .SaveContext import SaveContext
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])
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])
self.world.push_item(impa, item_to_place, False)
impa.locked = True
impa.event = True
self.world.itempool.remove(item_to_place)
# For now we will always output a patch file.
def generate_output(self, output_directory: str):
# Make ice traps appear as other random items
ice_traps = [loc.item for loc in self.get_locations() if loc.item.name == 'Ice Trap']
for trap in ice_traps:
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)}"
rom = Rom(file=get_options()['oot_options']['rom_file']) # a ROM must be provided, cannot produce patches without it
if self.hints != 'none':
buildWorldGossipHints(self)
patch_rom(self, rom)
patch_cosmetics(self, rom)
rom.update_header()
create_patch_file(rom, output_path(output_directory, outfile_name+'.apz5'))
rom.restore()
def generate_output(self, output_directory: str):
with i_o_limiter:
# Make ice traps appear as other random items
ice_traps = [loc.item for loc in self.get_locations() if loc.item.name == 'Ice Trap']
for trap in ice_traps:
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)}"
rom = Rom(
file=get_options()['oot_options']['rom_file']) # a ROM must be provided, cannot produce patches without it
if self.hints != 'none':
buildWorldGossipHints(self)
patch_rom(self, rom)
patch_cosmetics(self, rom)
rom.update_header()
create_patch_file(rom, output_path(output_directory, outfile_name + '.apz5'))
rom.restore()
# Helper functions
def get_shuffled_entrances(self):
@ -662,7 +673,7 @@ class OOTWorld(World):
def get_locations(self):
return [loc for region in self.regions for loc in region.locations]
def get_location(self, location):
def get_location(self, location):
return self.world.get_location(location, self.player)
def get_region(self, region):
@ -698,7 +709,6 @@ class OOTWorld(World):
return True
# 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.
def gather_hint_data(self):
@ -710,11 +720,11 @@ class OOTWorld(World):
items_by_region[r.hint_text] = {'dungeon': False, 'weight': 0, 'prog_items': 0}
for d in self.dungeons:
items_by_region[d.hint_text] = {'dungeon': True, 'weight': 0, 'prog_items': 0}
del(items_by_region["Link's Pocket"])
del(items_by_region[None])
del (items_by_region["Link's Pocket"])
del (items_by_region[None])
for loc in self.get_locations():
if loc.item.code: # is a real item
if loc.item.code: # is a real item
hint_area = get_hint_area(loc)
items_by_region[hint_area]['weight'] += 1
if loc.item.advancement and (not loc.locked or loc.item.type == 'Song'):
@ -726,9 +736,9 @@ class OOTWorld(World):
if not self.world.can_beat_game(state):
self.required_locations.append(loc)
self.empty_areas = {region: info for (region, info) in items_by_region.items() if not info['prog_items']}
for loc in self.world.get_filled_locations():
if (loc.item.player == self.player and self.is_major_item(loc.item)
if (loc.item.player == self.player and self.is_major_item(loc.item)
or (loc.item.player == self.player and loc.item.name in self.item_added_hint_types['item'])
or (loc.name in self.added_hint_types['item'] and loc.player == self.player)):
self.major_item_locations.append(loc)