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 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
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
||||||
|
|
|
@ -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]:
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue