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:
parent
51c38fc628
commit
1b27fc495f
|
@ -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)
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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 = []
|
||||
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue