mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-11 18:13:48 -07:00
Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e53b5324f5 | ||
|
|
25bbbdbecd | ||
|
|
d739d04380 | ||
|
|
f7da0265c4 | ||
|
|
82ae21420d | ||
|
|
89984a0d09 | ||
|
|
2e2ca1665b | ||
|
|
1b27fc495f | ||
|
|
51c38fc628 | ||
|
|
74c30ce09a | ||
|
|
859316353e | ||
|
|
63c9bea724 | ||
|
|
c73b994305 | ||
|
|
88451d4239 | ||
|
|
f74db254f6 | ||
|
|
3cb0a22e17 | ||
|
|
ca3e01b15e | ||
|
|
e9d1dcc46c | ||
|
|
7fd0f1a5bf | ||
|
|
2d65fbf798 | ||
|
|
ac915d00fc | ||
|
|
fbb8d6b132 | ||
|
|
fb0f70b3e3 | ||
|
|
17929415ee | ||
|
|
631b6788c6 | ||
|
|
7972aa6320 | ||
|
|
138c884684 | ||
|
|
f5ef98287a | ||
|
|
5188b41ab0 | ||
|
|
f83ba6e615 | ||
|
|
cc2a72eb82 | ||
|
|
4fcce66505 | ||
|
|
66627d8a66 | ||
|
|
adfd68f83c | ||
|
|
ddc619f2e7 | ||
|
|
ff2e57705e | ||
|
|
a6a859b272 | ||
|
|
88c5ebdd2f | ||
|
|
3d578bcc98 | ||
|
|
c3290af2bd | ||
|
|
01f1545b3e | ||
|
|
fc8e849db5 | ||
|
|
9115e59f15 | ||
|
|
2f4b248a45 | ||
|
|
2f28afb46e | ||
|
|
e960d7b58c | ||
|
|
321569c542 | ||
|
|
df037c54ff | ||
|
|
d859cecffb | ||
|
|
fd6e009c4b | ||
|
|
4520051ec9 | ||
|
|
b90b73859a | ||
|
|
6c357b61cc | ||
|
|
12957db90f | ||
|
|
3c74f561d5 | ||
|
|
cc70a6fa26 | ||
|
|
1c42564d90 | ||
|
|
e76c870c09 | ||
|
|
5daadcb2d5 | ||
|
|
a124a7a82a | ||
|
|
a65bf60cea | ||
|
|
3fa28a3fdb | ||
|
|
baa7992a7a | ||
|
|
7ba4bfc0d5 | ||
|
|
11fedef2f5 | ||
|
|
944347a2b3 | ||
|
|
8c72b0a6c4 | ||
|
|
5d62d4e063 | ||
|
|
9b05537a0e | ||
|
|
fd0a87626e | ||
|
|
9402d82405 | ||
|
|
da6674760c | ||
|
|
ee03371dd0 | ||
|
|
a975c8fd00 | ||
|
|
60840da740 | ||
|
|
de567cc701 | ||
|
|
de4775b0c8 | ||
|
|
104cc0ea83 | ||
|
|
5bb8de500a | ||
|
|
21255b3b46 | ||
|
|
e8da9924c6 | ||
|
|
96b38aba04 | ||
|
|
b8b51965d2 | ||
|
|
be46d128bc | ||
|
|
c05f1ed24f | ||
|
|
99775ec1bd | ||
|
|
f4f043ac87 | ||
|
|
acbca78e2d | ||
|
|
30ac7baa2c | ||
|
|
21a5170337 | ||
|
|
3a5a6a096b | ||
|
|
578ae70150 | ||
|
|
57282e76a4 | ||
|
|
7aaa652ef5 | ||
|
|
81da0d2ba4 | ||
|
|
ce6cdcaf92 | ||
|
|
4730a928b5 | ||
|
|
4c0f0a16c9 | ||
|
|
b07fc80f3f | ||
|
|
6a3d1fcaf4 | ||
|
|
4aeb3cd3dc | ||
|
|
6dc2000638 | ||
|
|
72610d8c2f | ||
|
|
0f55fa4f45 | ||
|
|
aec39c919c | ||
|
|
a0849f9416 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -4,9 +4,13 @@
|
||||
*_Spoiler.txt
|
||||
*.bmbp
|
||||
*.apbp
|
||||
*.apmc
|
||||
*.apz5
|
||||
*.pyc
|
||||
*.pyd
|
||||
*.sfc
|
||||
*.z64
|
||||
*.n64
|
||||
*.wixobj
|
||||
*.lck
|
||||
*.db3
|
||||
@@ -37,6 +41,7 @@ success.txt
|
||||
output/
|
||||
Output Logs/
|
||||
/factorio/
|
||||
/Minecraft Forge Server/
|
||||
/WebHostLib/static/generated
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
||||
171
BaseClasses.py
171
BaseClasses.py
@@ -6,7 +6,7 @@ import logging
|
||||
import json
|
||||
import functools
|
||||
from collections import OrderedDict, Counter, deque
|
||||
from typing import List, Dict, Optional, Set, Iterable, Union, Any
|
||||
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple
|
||||
import secrets
|
||||
import random
|
||||
|
||||
@@ -38,7 +38,7 @@ class MultiWorld():
|
||||
self.players = players
|
||||
self.glitch_triforce = False
|
||||
self.algorithm = 'balanced'
|
||||
self.dungeons = []
|
||||
self.dungeons: Dict[Tuple[str, int], Dungeon] = {}
|
||||
self.regions = []
|
||||
self.shops = []
|
||||
self.itempool = []
|
||||
@@ -95,10 +95,6 @@ class MultiWorld():
|
||||
set_player_attr('can_access_trock_big_chest', None)
|
||||
set_player_attr('can_access_trock_middle', None)
|
||||
set_player_attr('fix_fake_world', True)
|
||||
set_player_attr('mapshuffle', False)
|
||||
set_player_attr('compassshuffle', False)
|
||||
set_player_attr('keyshuffle', False)
|
||||
set_player_attr('bigkeyshuffle', False)
|
||||
set_player_attr('difficulty_requirements', None)
|
||||
set_player_attr('boss_shuffle', 'none')
|
||||
set_player_attr('enemy_shuffle', False)
|
||||
@@ -118,7 +114,6 @@ class MultiWorld():
|
||||
set_player_attr('blue_clock_time', 2)
|
||||
set_player_attr('green_clock_time', 4)
|
||||
set_player_attr('can_take_damage', True)
|
||||
set_player_attr('glitch_boots', True)
|
||||
set_player_attr('progression_balancing', True)
|
||||
set_player_attr('local_items', set())
|
||||
set_player_attr('non_local_items', set())
|
||||
@@ -158,6 +153,10 @@ class MultiWorld():
|
||||
def get_game_players(self, game_name: str):
|
||||
return tuple(player for player in self.player_ids if self.game[player] == game_name)
|
||||
|
||||
@functools.lru_cache()
|
||||
def get_game_worlds(self, game_name: str):
|
||||
return tuple(world for player, world in self.worlds.items() if self.game[player] == game_name)
|
||||
|
||||
def get_name_string_for_object(self, obj) -> str:
|
||||
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
|
||||
|
||||
@@ -209,42 +208,29 @@ class MultiWorld():
|
||||
return self._location_cache[location, player]
|
||||
|
||||
def get_dungeon(self, dungeonname: str, player: int) -> Dungeon:
|
||||
for dungeon in self.dungeons:
|
||||
if dungeon.name == dungeonname and dungeon.player == player:
|
||||
return dungeon
|
||||
raise KeyError('No such dungeon %s for player %d' % (dungeonname, player))
|
||||
try:
|
||||
return self.dungeons[dungeonname, player]
|
||||
except KeyError as e:
|
||||
raise KeyError('No such dungeon %s for player %d' % (dungeonname, player)) from e
|
||||
|
||||
def get_all_state(self, keys=False) -> CollectionState:
|
||||
key = f"_all_state_{keys}"
|
||||
cached = getattr(self, key, None)
|
||||
if cached:
|
||||
def get_all_state(self, use_cache: bool) -> CollectionState:
|
||||
cached = getattr(self, "_all_state", None)
|
||||
if use_cache and cached:
|
||||
return cached.copy()
|
||||
|
||||
ret = CollectionState(self)
|
||||
|
||||
for item in self.itempool:
|
||||
self.worlds[item.player].collect(ret, item)
|
||||
|
||||
if keys:
|
||||
for p in self.get_game_players("A Link to the Past"):
|
||||
world = self.worlds[p]
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
for item in ItemFactory(
|
||||
['Small Key (Hyrule Castle)', 'Big Key (Eastern Palace)', 'Big Key (Desert Palace)',
|
||||
'Small Key (Desert Palace)', 'Big Key (Tower of Hera)', 'Small Key (Tower of Hera)',
|
||||
'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)',
|
||||
'Big Key (Palace of Darkness)'] + ['Small Key (Palace of Darkness)'] * 6 + [
|
||||
'Big Key (Thieves Town)', 'Small Key (Thieves Town)', 'Big Key (Skull Woods)'] + [
|
||||
'Small Key (Skull Woods)'] * 3 + ['Big Key (Swamp Palace)',
|
||||
'Small Key (Swamp Palace)', 'Big Key (Ice Palace)'] + [
|
||||
'Small Key (Ice Palace)'] * 2 + ['Big Key (Misery Mire)', 'Big Key (Turtle Rock)',
|
||||
'Big Key (Ganons Tower)'] + [
|
||||
'Small Key (Misery Mire)'] * 3 + ['Small Key (Turtle Rock)'] * 4 + [
|
||||
'Small Key (Ganons Tower)'] * 4,
|
||||
p):
|
||||
world.collect(ret, item)
|
||||
from worlds.alttp.Dungeons import get_dungeon_item_pool
|
||||
for item in get_dungeon_item_pool(self):
|
||||
subworld = self.worlds[item.player]
|
||||
if item.name in subworld.dungeon_local_item_names:
|
||||
subworld.collect(ret, item)
|
||||
ret.sweep_for_events()
|
||||
setattr(self, key, ret)
|
||||
|
||||
if use_cache:
|
||||
self._all_state = ret
|
||||
return ret
|
||||
|
||||
def get_items(self) -> list:
|
||||
@@ -537,9 +523,7 @@ class CollectionState(object):
|
||||
locations = {location for location in locations if location.event}
|
||||
while new_locations:
|
||||
reachable_events = {location for location in locations if
|
||||
(not key_only or
|
||||
(not self.world.keyshuffle[location.item.player] and location.item.smallkey)
|
||||
or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey))
|
||||
(not key_only or getattr(location.item, "locked_dungeon_item", False))
|
||||
and location.can_reach(self)}
|
||||
new_locations = reachable_events - self.events
|
||||
for event in new_locations:
|
||||
@@ -569,13 +553,6 @@ class CollectionState(object):
|
||||
found += self.prog_items[item_name, player]
|
||||
return found
|
||||
|
||||
def has_key(self, item, player, count: int = 1):
|
||||
if self.world.logic[player] == 'nologic':
|
||||
return True
|
||||
if self.world.keyshuffle[player] == "universal":
|
||||
return self.can_buy_unlimited('Small Key (Universal)', player)
|
||||
return self.prog_items[item, player] >= count
|
||||
|
||||
def can_buy_unlimited(self, item: str, player: int) -> bool:
|
||||
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for
|
||||
shop in self.world.shops)
|
||||
@@ -807,13 +784,6 @@ class Region(object):
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_fill(self, item: Item):
|
||||
inside_dungeon_item = item.locked_dungeon_item
|
||||
if inside_dungeon_item:
|
||||
return self.dungeon.is_dungeon_item(item) and item.player == self.player
|
||||
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
@@ -855,8 +825,8 @@ class Entrance(object):
|
||||
world = self.parent_region.world if self.parent_region else None
|
||||
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
||||
|
||||
class Dungeon(object):
|
||||
|
||||
class Dungeon(object):
|
||||
def __init__(self, name: str, regions, big_key, small_keys, dungeon_items, player: int):
|
||||
self.name = name
|
||||
self.regions = regions
|
||||
@@ -911,12 +881,15 @@ class Boss():
|
||||
return f"Boss({self.name})"
|
||||
|
||||
class Location():
|
||||
shop_slot: bool = False
|
||||
# If given as integer, then this is the shop's inventory index
|
||||
shop_slot: Optional[int] = None
|
||||
shop_slot_disabled: bool = False
|
||||
event: bool = False
|
||||
locked: bool = False
|
||||
spot_type = 'Location'
|
||||
game: str = "Generic"
|
||||
show_in_spoiler: bool = True
|
||||
excluded: bool = False
|
||||
crystal: bool = False
|
||||
always_allow = staticmethod(lambda item, state: False)
|
||||
access_rule = staticmethod(lambda state: True)
|
||||
@@ -930,7 +903,7 @@ class Location():
|
||||
self.item: Optional[Item] = None
|
||||
|
||||
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
||||
return self.always_allow(state, item) or (self.parent_region.can_fill(item) and self.item_rule(item) and (not check_access or self.can_reach(state)))
|
||||
return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state)))
|
||||
|
||||
def can_reach(self, state: CollectionState) -> bool:
|
||||
# self.access_rule computes faster on average, so placing it first for faster abort
|
||||
@@ -966,21 +939,33 @@ class Location():
|
||||
|
||||
@property
|
||||
def hint_text(self):
|
||||
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
|
||||
hint_text = getattr(self, "_hint_text", None)
|
||||
if hint_text:
|
||||
return hint_text
|
||||
return "at " + self.name.replace("_", " ").replace("-", " ")
|
||||
|
||||
|
||||
class Item():
|
||||
location: Optional[Location] = None
|
||||
world: Optional[MultiWorld] = None
|
||||
code: Optional[str] = None # an item with ID None is called an Event, and does not get written to multidata
|
||||
game: str = "Generic"
|
||||
type: str = None
|
||||
never_exclude = False # change manually to ensure that a specific nonprogression item never goes on an excluded location
|
||||
# change manually to ensure that a specific non-progression item never goes on an excluded location
|
||||
never_exclude = False
|
||||
|
||||
# need to find a decent place for these to live and to allow other games to register texts if they want.
|
||||
pedestal_credit_text: str = "and the Unknown Item"
|
||||
sickkid_credit_text: Optional[str] = None
|
||||
magicshop_credit_text: Optional[str] = None
|
||||
zora_credit_text: Optional[str] = None
|
||||
fluteboy_credit_text: Optional[str] = None
|
||||
code: Optional[str] = None # an item with ID None is called an Event, and does not get written to multidata
|
||||
|
||||
# hopefully temporary attributes to satisfy legacy LttP code, proper implementation in subclass ALttPItem
|
||||
smallkey: bool = False
|
||||
bigkey: bool = False
|
||||
map: bool = False
|
||||
compass: bool = False
|
||||
|
||||
def __init__(self, name: str, advancement: bool, code: Optional[int], player: int):
|
||||
self.name = name
|
||||
@@ -1007,51 +992,6 @@ class Item():
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.player))
|
||||
|
||||
@property
|
||||
def crystal(self) -> bool:
|
||||
return self.type == 'Crystal'
|
||||
|
||||
@property
|
||||
def smallkey(self) -> bool:
|
||||
return self.type == 'SmallKey'
|
||||
|
||||
@property
|
||||
def bigkey(self) -> bool:
|
||||
return self.type == 'BigKey'
|
||||
|
||||
@property
|
||||
def map(self) -> bool:
|
||||
return self.type == 'Map'
|
||||
|
||||
@property
|
||||
def compass(self) -> bool:
|
||||
return self.type == 'Compass'
|
||||
|
||||
@property
|
||||
def dungeon_item(self) -> Optional[str]:
|
||||
if self.game == "A Link to the Past" and self.type in {"SmallKey", "BigKey", "Map", "Compass"}:
|
||||
return self.type
|
||||
|
||||
@property
|
||||
def shuffled_dungeon_item(self) -> bool:
|
||||
dungeon_item_type = self.dungeon_item
|
||||
if dungeon_item_type:
|
||||
return {"SmallKey" : self.world.keyshuffle,
|
||||
"BigKey": self.world.bigkeyshuffle,
|
||||
"Map": self.world.mapshuffle,
|
||||
"Compass": self.world.compassshuffle}[dungeon_item_type][self.player]
|
||||
return False
|
||||
|
||||
@property
|
||||
def locked_dungeon_item(self) -> bool:
|
||||
dungeon_item_type = self.dungeon_item
|
||||
if dungeon_item_type:
|
||||
return not {"SmallKey" : self.world.keyshuffle,
|
||||
"BigKey": self.world.bigkeyshuffle,
|
||||
"Map": self.world.mapshuffle,
|
||||
"Compass": self.world.compassshuffle}[dungeon_item_type][self.player]
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
@@ -1093,24 +1033,24 @@ class Spoiler():
|
||||
self.locations = OrderedDict()
|
||||
listed_locations = set()
|
||||
|
||||
lw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld]
|
||||
lw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld and loc.show_in_spoiler]
|
||||
self.locations['Light World'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in lw_locations])
|
||||
listed_locations.update(lw_locations)
|
||||
|
||||
dw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld]
|
||||
dw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld and loc.show_in_spoiler]
|
||||
self.locations['Dark World'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in dw_locations])
|
||||
listed_locations.update(dw_locations)
|
||||
|
||||
cave_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave]
|
||||
cave_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and loc.show_in_spoiler]
|
||||
self.locations['Caves'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in cave_locations])
|
||||
listed_locations.update(cave_locations)
|
||||
|
||||
for dungeon in self.world.dungeons:
|
||||
dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon]
|
||||
for dungeon in self.world.dungeons.values():
|
||||
dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and loc.show_in_spoiler]
|
||||
self.locations[str(dungeon)] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in dungeon_locations])
|
||||
listed_locations.update(dungeon_locations)
|
||||
|
||||
other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations]
|
||||
other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.show_in_spoiler]
|
||||
if other_locations:
|
||||
self.locations['Other Locations'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in other_locations])
|
||||
listed_locations.update(other_locations)
|
||||
@@ -1179,10 +1119,6 @@ class Spoiler():
|
||||
'open_pyramid': self.world.open_pyramid,
|
||||
'accessibility': self.world.accessibility,
|
||||
'hints': self.world.hints,
|
||||
'mapshuffle': self.world.mapshuffle,
|
||||
'compassshuffle': self.world.compassshuffle,
|
||||
'keyshuffle': self.world.keyshuffle,
|
||||
'bigkeyshuffle': self.world.bigkeyshuffle,
|
||||
'boss_shuffle': self.world.boss_shuffle,
|
||||
'enemy_shuffle': self.world.enemy_shuffle,
|
||||
'enemy_health': self.world.enemy_health,
|
||||
@@ -1277,15 +1213,6 @@ class Spoiler():
|
||||
outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player])
|
||||
outfile.write('Pyramid hole pre-opened: %s\n' % (
|
||||
'Yes' if self.metadata['open_pyramid'][player] else 'No'))
|
||||
|
||||
outfile.write('Map shuffle: %s\n' %
|
||||
('Yes' if self.metadata['mapshuffle'][player] else 'No'))
|
||||
outfile.write('Compass shuffle: %s\n' %
|
||||
('Yes' if self.metadata['compassshuffle'][player] else 'No'))
|
||||
outfile.write(
|
||||
'Small Key shuffle: %s\n' % (bool_to_text(self.metadata['keyshuffle'][player])))
|
||||
outfile.write('Big Key shuffle: %s\n' % (
|
||||
'Yes' if self.metadata['bigkeyshuffle'][player] else 'No'))
|
||||
outfile.write('Shop inventory shuffle: %s\n' %
|
||||
bool_to_text("i" in self.metadata["shop_shuffle"][player]))
|
||||
outfile.write('Shop price shuffle: %s\n' %
|
||||
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
import typing
|
||||
import asyncio
|
||||
import urllib.parse
|
||||
import sys
|
||||
|
||||
import websockets
|
||||
|
||||
@@ -14,6 +15,7 @@ from worlds import network_data_package, AutoWorldRegister
|
||||
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
gui_enabled = Utils.is_frozen() or "--nogui" not in sys.argv
|
||||
|
||||
class ClientCommandProcessor(CommandProcessor):
|
||||
def __init__(self, ctx: CommonContext):
|
||||
|
||||
@@ -4,14 +4,13 @@ import logging
|
||||
import json
|
||||
import string
|
||||
import copy
|
||||
import sys
|
||||
import subprocess
|
||||
import factorio_rcon
|
||||
|
||||
import colorama
|
||||
import asyncio
|
||||
from queue import Queue
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled
|
||||
from MultiServer import mark_raw
|
||||
|
||||
import Utils
|
||||
@@ -20,17 +19,17 @@ from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePar
|
||||
|
||||
from worlds.factorio import Factorio
|
||||
|
||||
os.makedirs("logs", exist_ok=True)
|
||||
log_folder = Utils.local_path("logs")
|
||||
|
||||
# Log to file in gui case
|
||||
if getattr(sys, "frozen", False) and not "--nogui" in sys.argv:
|
||||
os.makedirs(log_folder, exist_ok=True)
|
||||
|
||||
|
||||
if gui_enabled:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
|
||||
filename=os.path.join("logs", "FactorioClient.txt"), filemode="w", force=True)
|
||||
filename=os.path.join(log_folder, "FactorioClient.txt"), filemode="w", force=True)
|
||||
else:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
|
||||
logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w"))
|
||||
|
||||
gui_enabled = Utils.is_frozen() or "--nogui" not in sys.argv
|
||||
logging.getLogger().addHandler(logging.FileHandler(os.path.join(log_folder, "FactorioClient.txt"), "w"))
|
||||
|
||||
|
||||
class FactorioCommandProcessor(ClientCommandProcessor):
|
||||
@@ -112,9 +111,10 @@ class FactorioContext(CommonContext):
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == "Connected":
|
||||
# catch up sync anything that is already cleared.
|
||||
for tech in args["checked_locations"]:
|
||||
item_name = f"ap-{tech}-"
|
||||
self.rcon_client.send_command(f'/ap-get-technology {item_name}\t-1')
|
||||
if args["checked_locations"]:
|
||||
self.rcon_client.send_commands({item_name: f'/ap-get-technology ap-{item_name}-\t-1' for
|
||||
item_name in args["checked_locations"]})
|
||||
|
||||
|
||||
async def game_watcher(ctx: FactorioContext):
|
||||
bridge_logger = logging.getLogger("FactorioWatcher")
|
||||
@@ -198,11 +198,15 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
if ctx.mod_version < Utils.Version(0, 1, 6):
|
||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||
if not ctx.server:
|
||||
logger.info("Established bridge to Factorio Server. "
|
||||
"Ready to connect to Archipelago via /connect")
|
||||
|
||||
if not ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg:
|
||||
ctx.awaiting_bridge = True
|
||||
|
||||
if ctx.rcon_client:
|
||||
commands = {}
|
||||
while ctx.send_index < len(ctx.items_received):
|
||||
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
|
||||
item_id = transfer_item.item
|
||||
@@ -212,8 +216,10 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
else:
|
||||
item_name = Factorio.item_id_to_name[item_id]
|
||||
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.")
|
||||
ctx.rcon_client.send_command(f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}')
|
||||
commands[ctx.send_index] = f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}'
|
||||
ctx.send_index += 1
|
||||
if commands:
|
||||
ctx.rcon_client.send_commands(commands)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
except Exception as e:
|
||||
@@ -354,13 +360,13 @@ if __name__ == '__main__':
|
||||
factorio_server_logger = logging.getLogger("FactorioServer")
|
||||
options = Utils.get_options()
|
||||
executable = options["factorio_options"]["executable"]
|
||||
bin_dir = os.path.dirname(executable)
|
||||
if not os.path.exists(bin_dir):
|
||||
raise FileNotFoundError(f"Path {bin_dir} does not exist or could not be accessed.")
|
||||
if not os.path.isdir(bin_dir):
|
||||
raise NotADirectoryError(f"Path {bin_dir} is not a directory.")
|
||||
if not os.path.exists(executable):
|
||||
if os.path.exists(executable + ".exe"):
|
||||
|
||||
if not os.path.exists(os.path.dirname(executable)):
|
||||
raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
|
||||
if os.path.isdir(executable): # user entered a path to a directory, let's find the executable therein
|
||||
executable = os.path.join(executable, "factorio")
|
||||
if not os.path.isfile(executable):
|
||||
if os.path.isfile(executable + ".exe"):
|
||||
executable = executable + ".exe"
|
||||
else:
|
||||
raise FileNotFoundError(f"Path {executable} is not an executable file.")
|
||||
|
||||
23
Fill.py
23
Fill.py
@@ -4,8 +4,6 @@ import collections
|
||||
import itertools
|
||||
|
||||
from BaseClasses import CollectionState, Location, MultiWorld
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
from worlds.alttp.Regions import key_drop_data
|
||||
from worlds.generic import PlandoItem
|
||||
from worlds.AutoWorld import call_all
|
||||
|
||||
@@ -81,6 +79,7 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
|
||||
progitempool = []
|
||||
nonexcludeditempool = []
|
||||
localrestitempool = {player: [] for player in range(1, world.players + 1)}
|
||||
nonlocalrestitempool = []
|
||||
restitempool = []
|
||||
|
||||
for item in world.itempool:
|
||||
@@ -90,11 +89,13 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
|
||||
nonexcludeditempool.append(item)
|
||||
elif item.name in world.local_items[item.player]:
|
||||
localrestitempool[item.player].append(item)
|
||||
elif item.name in world.non_local_items[item.player]:
|
||||
nonlocalrestitempool.append(item)
|
||||
else:
|
||||
restitempool.append(item)
|
||||
|
||||
world.random.shuffle(fill_locations)
|
||||
call_all(world, "fill_hook", progitempool, nonexcludeditempool, localrestitempool, restitempool, fill_locations)
|
||||
call_all(world, "fill_hook", progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations)
|
||||
|
||||
fill_restrictive(world, world.state, fill_locations, progitempool)
|
||||
|
||||
@@ -120,14 +121,22 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
fill_locations.remove(spot_to_fill)
|
||||
|
||||
for item_to_place in nonlocalrestitempool:
|
||||
for i, location in enumerate(fill_locations):
|
||||
if location.player != item_to_place.player:
|
||||
world.push_item(fill_locations.pop(i), item_to_place, False)
|
||||
break
|
||||
else:
|
||||
logging.warning(f"Could not place non_local_item {item_to_place} among {fill_locations}, tossing.")
|
||||
|
||||
world.random.shuffle(fill_locations)
|
||||
|
||||
restitempool, fill_locations = fast_fill(world, restitempool, fill_locations)
|
||||
unplaced = [item for item in progitempool + restitempool]
|
||||
unplaced = progitempool + restitempool
|
||||
unfilled = [location.name for location in fill_locations]
|
||||
|
||||
if unplaced or unfilled:
|
||||
raise FillError(f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
|
||||
logging.warning(f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
|
||||
|
||||
|
||||
def fast_fill(world: MultiWorld, item_pool: typing.List, fill_locations: typing.List) -> typing.Tuple[typing.List, typing.List]:
|
||||
@@ -328,6 +337,8 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked=
|
||||
|
||||
|
||||
def distribute_planned(world: MultiWorld):
|
||||
# TODO: remove. Preferably by implementing key drop
|
||||
from worlds.alttp.Regions import key_drop_data
|
||||
world_name_lookup = world.world_name_lookup
|
||||
|
||||
for player in world.player_ids:
|
||||
@@ -338,7 +349,7 @@ def distribute_planned(world: MultiWorld):
|
||||
placement.warn(
|
||||
f"Can't place '{placement.item}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
|
||||
continue
|
||||
item = ItemFactory(placement.item, player)
|
||||
item = world.worlds[player].create_item(placement.item)
|
||||
target_world: int = placement.world
|
||||
if target_world is False or world.players == 1:
|
||||
target_world = player # in own world
|
||||
|
||||
171
Generate.py
171
Generate.py
@@ -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
|
||||
@@ -120,7 +120,6 @@ def main(args=None, callback=ERmain):
|
||||
f"A mix is also permitted.")
|
||||
erargs = parse_arguments(['--multi', str(args.multi)])
|
||||
erargs.seed = seed
|
||||
erargs.name = {x: "" for x in range(1, args.multi + 1)} # only so it can be overwrittin in mystery
|
||||
erargs.create_spoiler = args.spoiler > 0
|
||||
erargs.glitch_triforce = options["generator"]["glitch_triforce_room"]
|
||||
erargs.race = args.race
|
||||
@@ -189,6 +188,9 @@ def main(args=None, callback=ERmain):
|
||||
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
||||
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
||||
|
||||
if len(set(erargs.name.values())) != len(erargs.name):
|
||||
raise Exception(f"Names have to be unique. Names: {erargs.name}")
|
||||
|
||||
if args.yaml_output:
|
||||
import yaml
|
||||
important = {}
|
||||
@@ -237,7 +239,7 @@ def convert_to_on_off(value):
|
||||
return {True: "on", False: "off"}.get(value, value)
|
||||
|
||||
|
||||
def get_choice(option, root, value=None) -> typing.Any:
|
||||
def get_choice_legacy(option, root, value=None) -> typing.Any:
|
||||
if option not in root:
|
||||
return value
|
||||
if type(root[option]) is list:
|
||||
@@ -252,6 +254,20 @@ def get_choice(option, root, value=None) -> typing.Any:
|
||||
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
|
||||
|
||||
|
||||
def get_choice(option, root, value=None) -> typing.Any:
|
||||
if option not in root:
|
||||
return value
|
||||
if type(root[option]) is list:
|
||||
return random.choices(root[option])[0]
|
||||
if type(root[option]) is not dict:
|
||||
return root[option]
|
||||
if not root[option]:
|
||||
return value
|
||||
if any(root[option].values()):
|
||||
return random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0]
|
||||
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
|
||||
|
||||
|
||||
class SafeDict(dict):
|
||||
def __missing__(self, key):
|
||||
return '{' + key + '}'
|
||||
@@ -465,7 +481,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
inventoryweights = game_weights.get('start_inventory', {})
|
||||
startitems = []
|
||||
for item in inventoryweights.keys():
|
||||
itemvalue = get_choice(item, inventoryweights)
|
||||
itemvalue = get_choice_legacy(item, inventoryweights)
|
||||
if isinstance(itemvalue, int):
|
||||
for i in range(int(itemvalue)):
|
||||
startitems.append(item)
|
||||
@@ -485,7 +501,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
for option_name, option in AutoWorldRegister.world_types[ret.game].options.items():
|
||||
if option_name in game_weights:
|
||||
try:
|
||||
if issubclass(option, Options.OptionDict):
|
||||
if issubclass(option, Options.OptionDict) or issubclass(option, Options.OptionList):
|
||||
setattr(ret, option_name, option.from_any(game_weights[option_name]))
|
||||
else:
|
||||
setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights)))
|
||||
@@ -513,7 +529,9 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
|
||||
|
||||
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
glitches_required = get_choice('glitches_required', weights)
|
||||
if "dungeon_items" in weights and get_choice_legacy('dungeon_items', weights, "none") != "none":
|
||||
raise Exception(f"dungeon_items key in A Link to the Past was removed, but is present in these weights as {get_choice_legacy('dungeon_items', weights, False)}.")
|
||||
glitches_required = get_choice_legacy('glitches_required', weights)
|
||||
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']:
|
||||
logging.warning("Only NMG, OWG, HMG and No Logic supported")
|
||||
glitches_required = 'none'
|
||||
@@ -521,7 +539,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[
|
||||
glitches_required]
|
||||
|
||||
ret.dark_room_logic = get_choice("dark_room_logic", weights, "lamp")
|
||||
ret.dark_room_logic = get_choice_legacy("dark_room_logic", weights, "lamp")
|
||||
if not ret.dark_room_logic: # None/False
|
||||
ret.dark_room_logic = "none"
|
||||
if ret.dark_room_logic == "sconces":
|
||||
@@ -529,94 +547,78 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
if ret.dark_room_logic not in {"lamp", "torches", "none"}:
|
||||
raise ValueError(f"Unknown Dark Room Logic: \"{ret.dark_room_logic}\"")
|
||||
|
||||
ret.restrict_dungeon_item_on_boss = get_choice('restrict_dungeon_item_on_boss', weights, False)
|
||||
ret.restrict_dungeon_item_on_boss = get_choice_legacy('restrict_dungeon_item_on_boss', weights, False)
|
||||
|
||||
dungeon_items = get_choice('dungeon_items', weights)
|
||||
if dungeon_items == 'full' or dungeon_items == True:
|
||||
dungeon_items = 'mcsb'
|
||||
elif dungeon_items == 'standard':
|
||||
dungeon_items = ""
|
||||
elif not dungeon_items:
|
||||
dungeon_items = ""
|
||||
if "u" in dungeon_items:
|
||||
dungeon_items.replace("s", "")
|
||||
|
||||
ret.mapshuffle = get_choice('map_shuffle', weights, 'm' in dungeon_items)
|
||||
ret.compassshuffle = get_choice('compass_shuffle', weights, 'c' in dungeon_items)
|
||||
ret.keyshuffle = get_choice('smallkey_shuffle', weights,
|
||||
'universal' if 'u' in dungeon_items else 's' in dungeon_items)
|
||||
ret.bigkeyshuffle = get_choice('bigkey_shuffle', weights, 'b' in dungeon_items)
|
||||
|
||||
entrance_shuffle = get_choice('entrance_shuffle', weights, 'vanilla')
|
||||
entrance_shuffle = get_choice_legacy('entrance_shuffle', weights, 'vanilla')
|
||||
if entrance_shuffle.startswith('none-'):
|
||||
ret.shuffle = 'vanilla'
|
||||
else:
|
||||
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
|
||||
|
||||
goal = get_choice('goals', weights, 'ganon')
|
||||
goal = get_choice_legacy('goals', weights, 'ganon')
|
||||
|
||||
ret.goal = goals[goal]
|
||||
|
||||
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
|
||||
# fast ganon + ganon at hole
|
||||
ret.open_pyramid = get_choice('open_pyramid', weights, 'goal')
|
||||
ret.open_pyramid = get_choice_legacy('open_pyramid', weights, 'goal')
|
||||
|
||||
extra_pieces = get_choice('triforce_pieces_mode', weights, 'available')
|
||||
extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available')
|
||||
|
||||
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice('triforce_pieces_required', weights, 20))
|
||||
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice_legacy('triforce_pieces_required', weights, 20))
|
||||
|
||||
# sum a percentage to required
|
||||
if extra_pieces == 'percentage':
|
||||
percentage = max(100, float(get_choice('triforce_pieces_percentage', weights, 150))) / 100
|
||||
percentage = max(100, float(get_choice_legacy('triforce_pieces_percentage', weights, 150))) / 100
|
||||
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
|
||||
# vanilla mode (specify how many pieces are)
|
||||
elif extra_pieces == 'available':
|
||||
ret.triforce_pieces_available = LttPOptions.TriforcePieces.from_any(
|
||||
get_choice('triforce_pieces_available', weights, 30))
|
||||
get_choice_legacy('triforce_pieces_available', weights, 30))
|
||||
# required pieces + fixed extra
|
||||
elif extra_pieces == 'extra':
|
||||
extra_pieces = max(0, int(get_choice('triforce_pieces_extra', weights, 10)))
|
||||
extra_pieces = max(0, int(get_choice_legacy('triforce_pieces_extra', weights, 10)))
|
||||
ret.triforce_pieces_available = ret.triforce_pieces_required + extra_pieces
|
||||
|
||||
# change minimum to required pieces to avoid problems
|
||||
ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90)
|
||||
|
||||
ret.shop_shuffle = get_choice('shop_shuffle', weights, '')
|
||||
ret.shop_shuffle = get_choice_legacy('shop_shuffle', weights, '')
|
||||
if not ret.shop_shuffle:
|
||||
ret.shop_shuffle = ''
|
||||
|
||||
ret.mode = get_choice("mode", weights)
|
||||
ret.retro = get_choice("retro", weights)
|
||||
ret.mode = get_choice_legacy("mode", weights)
|
||||
ret.retro = get_choice_legacy("retro", weights)
|
||||
|
||||
ret.hints = get_choice('hints', weights)
|
||||
ret.hints = get_choice_legacy('hints', weights)
|
||||
|
||||
ret.swordless = get_choice('swordless', weights, False)
|
||||
ret.swordless = get_choice_legacy('swordless', weights, False)
|
||||
|
||||
ret.difficulty = get_choice('item_pool', weights)
|
||||
ret.difficulty = get_choice_legacy('item_pool', weights)
|
||||
|
||||
ret.item_functionality = get_choice('item_functionality', weights)
|
||||
ret.item_functionality = get_choice_legacy('item_functionality', weights)
|
||||
|
||||
boss_shuffle = get_choice('boss_shuffle', weights)
|
||||
boss_shuffle = get_choice_legacy('boss_shuffle', weights)
|
||||
ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options)
|
||||
|
||||
ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False))
|
||||
ret.enemy_shuffle = bool(get_choice_legacy('enemy_shuffle', weights, False))
|
||||
|
||||
ret.killable_thieves = get_choice('killable_thieves', weights, False)
|
||||
ret.tile_shuffle = get_choice('tile_shuffle', weights, False)
|
||||
ret.bush_shuffle = get_choice('bush_shuffle', weights, False)
|
||||
ret.killable_thieves = get_choice_legacy('killable_thieves', weights, False)
|
||||
ret.tile_shuffle = get_choice_legacy('tile_shuffle', weights, False)
|
||||
ret.bush_shuffle = get_choice_legacy('bush_shuffle', weights, False)
|
||||
|
||||
ret.enemy_damage = {None: 'default',
|
||||
'default': 'default',
|
||||
'shuffled': 'shuffled',
|
||||
'random': 'chaos', # to be removed
|
||||
'chaos': 'chaos',
|
||||
}[get_choice('enemy_damage', weights)]
|
||||
}[get_choice_legacy('enemy_damage', weights)]
|
||||
|
||||
ret.enemy_health = get_choice('enemy_health', weights)
|
||||
ret.enemy_health = get_choice_legacy('enemy_health', weights)
|
||||
|
||||
ret.shufflepots = get_choice('pot_shuffle', weights)
|
||||
ret.shufflepots = get_choice_legacy('pot_shuffle', weights)
|
||||
|
||||
ret.beemizer = int(get_choice('beemizer', weights, 0))
|
||||
ret.beemizer = int(get_choice_legacy('beemizer', weights, 0))
|
||||
|
||||
ret.timer = {'none': False,
|
||||
None: False,
|
||||
@@ -625,19 +627,19 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
'timed_ohko': 'timed-ohko',
|
||||
'ohko': 'ohko',
|
||||
'timed_countdown': 'timed-countdown',
|
||||
'display': 'display'}[get_choice('timer', weights, False)]
|
||||
'display': 'display'}[get_choice_legacy('timer', weights, False)]
|
||||
|
||||
ret.countdown_start_time = int(get_choice('countdown_start_time', weights, 10))
|
||||
ret.red_clock_time = int(get_choice('red_clock_time', weights, -2))
|
||||
ret.blue_clock_time = int(get_choice('blue_clock_time', weights, 2))
|
||||
ret.green_clock_time = int(get_choice('green_clock_time', weights, 4))
|
||||
ret.countdown_start_time = int(get_choice_legacy('countdown_start_time', weights, 10))
|
||||
ret.red_clock_time = int(get_choice_legacy('red_clock_time', weights, -2))
|
||||
ret.blue_clock_time = int(get_choice_legacy('blue_clock_time', weights, 2))
|
||||
ret.green_clock_time = int(get_choice_legacy('green_clock_time', weights, 4))
|
||||
|
||||
ret.dungeon_counters = get_choice('dungeon_counters', weights, 'default')
|
||||
ret.dungeon_counters = get_choice_legacy('dungeon_counters', weights, 'default')
|
||||
|
||||
ret.shuffle_prizes = get_choice('shuffle_prizes', weights, "g")
|
||||
ret.shuffle_prizes = get_choice_legacy('shuffle_prizes', weights, "g")
|
||||
|
||||
ret.required_medallions = [get_choice("misery_mire_medallion", weights, "random"),
|
||||
get_choice("turtle_rock_medallion", weights, "random")]
|
||||
ret.required_medallions = [get_choice_legacy("misery_mire_medallion", weights, "random"),
|
||||
get_choice_legacy("turtle_rock_medallion", weights, "random")]
|
||||
|
||||
for index, medallion in enumerate(ret.required_medallions):
|
||||
ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"} \
|
||||
@@ -645,13 +647,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
if not ret.required_medallions[index]:
|
||||
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
|
||||
|
||||
ret.glitch_boots = get_choice('glitch_boots', weights, True)
|
||||
|
||||
if get_choice("local_keys", weights, "l" in dungeon_items):
|
||||
# () important for ordering of commands, without them the Big Keys section is part of the Small Key else
|
||||
ret.local_items |= item_name_groups["Small Keys"] if ret.keyshuffle else set()
|
||||
ret.local_items |= item_name_groups["Big Keys"] if ret.bigkeyshuffle else set()
|
||||
|
||||
ret.plando_items = []
|
||||
if "items" in plando_options:
|
||||
|
||||
@@ -665,10 +660,10 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
|
||||
options = weights.get("plando_items", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
from_pool = get_choice("from_pool", placement, PlandoItem._field_defaults["from_pool"])
|
||||
location_world = get_choice("world", placement, PlandoItem._field_defaults["world"])
|
||||
force = str(get_choice("force", placement, PlandoItem._field_defaults["force"])).lower()
|
||||
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
|
||||
from_pool = get_choice_legacy("from_pool", placement, PlandoItem._field_defaults["from_pool"])
|
||||
location_world = get_choice_legacy("world", placement, PlandoItem._field_defaults["world"])
|
||||
force = str(get_choice_legacy("force", placement, PlandoItem._field_defaults["force"])).lower()
|
||||
if "items" in placement and "locations" in placement:
|
||||
items = placement["items"]
|
||||
locations = placement["locations"]
|
||||
@@ -684,8 +679,8 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
for item, location in zip(items, locations):
|
||||
add_plando_item(item, location)
|
||||
else:
|
||||
item = get_choice("item", placement, get_choice("items", placement))
|
||||
location = get_choice("location", placement)
|
||||
item = get_choice_legacy("item", placement, get_choice_legacy("items", placement))
|
||||
location = get_choice_legacy("location", placement)
|
||||
add_plando_item(item, location)
|
||||
|
||||
ret.plando_texts = {}
|
||||
@@ -694,39 +689,39 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
tt.removeUnwantedText()
|
||||
options = weights.get("plando_texts", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
at = str(get_choice("at", placement))
|
||||
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
|
||||
at = str(get_choice_legacy("at", placement))
|
||||
if at not in tt:
|
||||
raise Exception(f"No text target \"{at}\" found.")
|
||||
ret.plando_texts[at] = str(get_choice("text", placement))
|
||||
ret.plando_texts[at] = str(get_choice_legacy("text", placement))
|
||||
|
||||
ret.plando_connections = []
|
||||
if "connections" in plando_options:
|
||||
options = weights.get("plando_connections", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
|
||||
ret.plando_connections.append(PlandoConnection(
|
||||
get_choice("entrance", placement),
|
||||
get_choice("exit", placement),
|
||||
get_choice("direction", placement, "both")
|
||||
get_choice_legacy("entrance", placement),
|
||||
get_choice_legacy("exit", placement),
|
||||
get_choice_legacy("direction", placement, "both")
|
||||
))
|
||||
|
||||
ret.sprite_pool = weights.get('sprite_pool', [])
|
||||
ret.sprite = get_choice('sprite', weights, "Link")
|
||||
ret.sprite = get_choice_legacy('sprite', weights, "Link")
|
||||
if 'random_sprite_on_event' in weights:
|
||||
randomoneventweights = weights['random_sprite_on_event']
|
||||
if get_choice('enabled', randomoneventweights, False):
|
||||
if get_choice_legacy('enabled', randomoneventweights, False):
|
||||
ret.sprite = 'randomon'
|
||||
ret.sprite += '-hit' if get_choice('on_hit', randomoneventweights, True) else ''
|
||||
ret.sprite += '-enter' if get_choice('on_enter', randomoneventweights, False) else ''
|
||||
ret.sprite += '-exit' if get_choice('on_exit', randomoneventweights, False) else ''
|
||||
ret.sprite += '-slash' if get_choice('on_slash', randomoneventweights, False) else ''
|
||||
ret.sprite += '-item' if get_choice('on_item', randomoneventweights, False) else ''
|
||||
ret.sprite += '-bonk' if get_choice('on_bonk', randomoneventweights, False) else ''
|
||||
ret.sprite = 'randomonall' if get_choice('on_everything', randomoneventweights, False) else ret.sprite
|
||||
ret.sprite += '-hit' if get_choice_legacy('on_hit', randomoneventweights, True) else ''
|
||||
ret.sprite += '-enter' if get_choice_legacy('on_enter', randomoneventweights, False) else ''
|
||||
ret.sprite += '-exit' if get_choice_legacy('on_exit', randomoneventweights, False) else ''
|
||||
ret.sprite += '-slash' if get_choice_legacy('on_slash', randomoneventweights, False) else ''
|
||||
ret.sprite += '-item' if get_choice_legacy('on_item', randomoneventweights, False) else ''
|
||||
ret.sprite += '-bonk' if get_choice_legacy('on_bonk', randomoneventweights, False) else ''
|
||||
ret.sprite = 'randomonall' if get_choice_legacy('on_everything', randomoneventweights, False) else ret.sprite
|
||||
ret.sprite = 'randomonnone' if ret.sprite == 'randomon' else ret.sprite
|
||||
|
||||
if (not ret.sprite_pool or get_choice('use_weighted_sprite_pool', randomoneventweights, False)) \
|
||||
if (not ret.sprite_pool or get_choice_legacy('use_weighted_sprite_pool', randomoneventweights, False)) \
|
||||
and 'sprite' in weights: # Use sprite as a weighted sprite pool, if a sprite pool is not already defined.
|
||||
for key, value in weights['sprite'].items():
|
||||
if key.startswith('random'):
|
||||
@@ -740,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)
|
||||
|
||||
@@ -3,12 +3,13 @@ import atexit
|
||||
exit_func = atexit.register(input, "Press enter to close.")
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
import multiprocessing
|
||||
import os
|
||||
import subprocess
|
||||
import base64
|
||||
import shutil
|
||||
import logging
|
||||
import asyncio
|
||||
from json import loads, dumps
|
||||
|
||||
from Utils import get_item_name_from_id
|
||||
@@ -25,21 +26,22 @@ from NetUtils import *
|
||||
from worlds.alttp import Regions, Shops
|
||||
from worlds.alttp import Items
|
||||
import Utils
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, gui_enabled
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
from MultiServer import mark_raw
|
||||
|
||||
os.makedirs("logs", exist_ok=True)
|
||||
log_folder = Utils.local_path("logs")
|
||||
os.makedirs(log_folder, exist_ok=True)
|
||||
|
||||
# Log to file in gui case
|
||||
if getattr(sys, "frozen", False) and not "--nogui" in sys.argv:
|
||||
if gui_enabled:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
|
||||
filename=os.path.join("logs", "LttPClient.txt"), filemode="w", force=True)
|
||||
filename=os.path.join(log_folder, "LttPClient.txt"), filemode="w", force=True)
|
||||
else:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
|
||||
logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "LttPClient.txt"), "w"))
|
||||
logging.getLogger().addHandler(logging.FileHandler(os.path.join(log_folder, "LttPClient.txt"), "w"))
|
||||
|
||||
|
||||
class LttPCommandProcessor(ClientCommandProcessor):
|
||||
@@ -53,11 +55,26 @@ class LttPCommandProcessor(ClientCommandProcessor):
|
||||
self.output(f"Setting slow mode to {self.ctx.slow_mode}")
|
||||
|
||||
@mark_raw
|
||||
def _cmd_snes(self, snes_address: str = "") -> bool:
|
||||
"""Connect to a snes.
|
||||
Optionally include network address of a snes to connect to, otherwise show available devices"""
|
||||
def _cmd_snes(self, snes_options: str = "") -> bool:
|
||||
"""Connect to a snes. Optionally include network address of a snes to connect to, otherwise show available devices; and a SNES device number if more than one SNES is detected"""
|
||||
|
||||
snes_address = self.ctx.snes_address
|
||||
snes_device_number = -1
|
||||
|
||||
options = snes_options.split()
|
||||
num_options = len(options)
|
||||
|
||||
if num_options > 0:
|
||||
snes_address = options[0]
|
||||
|
||||
if num_options > 1:
|
||||
try:
|
||||
snes_device_number = int(options[1])
|
||||
except:
|
||||
pass
|
||||
|
||||
self.ctx.snes_reconnect_address = None
|
||||
asyncio.create_task(snes_connect(self.ctx, snes_address if snes_address else self.ctx.snes_address))
|
||||
asyncio.create_task(snes_connect(self.ctx, snes_address, snes_device_number))
|
||||
return True
|
||||
|
||||
def _cmd_snes_close(self) -> bool:
|
||||
@@ -496,7 +513,7 @@ async def get_snes_devices(ctx: Context):
|
||||
return devices
|
||||
|
||||
|
||||
async def snes_connect(ctx: Context, address):
|
||||
async def snes_connect(ctx: Context, address, deviceIndex = -1):
|
||||
global SNES_RECONNECT_DELAY
|
||||
if ctx.snes_socket is not None and ctx.snes_state == SNESState.SNES_CONNECTED:
|
||||
if ctx.rom:
|
||||
@@ -505,6 +522,7 @@ async def snes_connect(ctx: Context, address):
|
||||
snes_logger.error('Already connected to SNI, likely awaiting a device.')
|
||||
return
|
||||
|
||||
device = None
|
||||
recv_task = None
|
||||
ctx.snes_state = SNESState.SNES_CONNECTING
|
||||
socket = await _snes_connect(ctx, address)
|
||||
@@ -513,15 +531,29 @@ async def snes_connect(ctx: Context, address):
|
||||
|
||||
try:
|
||||
devices = await get_snes_devices(ctx)
|
||||
numDevices = len(devices)
|
||||
|
||||
if len(devices) == 1:
|
||||
if numDevices == 1:
|
||||
device = devices[0]
|
||||
elif ctx.snes_reconnect_address:
|
||||
if ctx.snes_attached_device[1] in devices:
|
||||
device = ctx.snes_attached_device[1]
|
||||
else:
|
||||
device = devices[ctx.snes_attached_device[0]]
|
||||
else:
|
||||
elif numDevices > 1:
|
||||
if deviceIndex == -1:
|
||||
snes_logger.info("Found " + str(numDevices) + " SNES devices; connect to one with /snes <address> <device number>:")
|
||||
|
||||
for idx, availableDevice in enumerate(devices):
|
||||
snes_logger.info(str(idx + 1) + ": " + availableDevice)
|
||||
|
||||
elif (deviceIndex < 0) or (deviceIndex - 1) > numDevices:
|
||||
snes_logger.warning("SNES device number out of range")
|
||||
|
||||
else:
|
||||
device = devices[deviceIndex - 1]
|
||||
|
||||
if device is None:
|
||||
await snes_disconnect(ctx)
|
||||
return
|
||||
|
||||
@@ -888,7 +920,7 @@ async def main():
|
||||
meta, romfile = Patch.create_rom_file(args.diff_file)
|
||||
args.connect = meta["server"]
|
||||
logging.info(f"Wrote rom file to {romfile}")
|
||||
adjustedromfile, adjusted = Utils.get_adjuster_settings(romfile)
|
||||
adjustedromfile, adjusted = Utils.get_adjuster_settings(romfile, gui_enabled)
|
||||
if adjusted:
|
||||
try:
|
||||
shutil.move(adjustedromfile, romfile)
|
||||
@@ -901,7 +933,7 @@ async def main():
|
||||
if ctx.server_task is None:
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
|
||||
if Utils.is_frozen() or "--nogui" not in sys.argv:
|
||||
if gui_enabled:
|
||||
input_task = None
|
||||
from kvui import LttPManager
|
||||
ctx.ui = LttPManager(ctx)
|
||||
|
||||
373
Main.py
373
Main.py
@@ -14,8 +14,7 @@ from BaseClasses import MultiWorld, CollectionState, Region, RegionType
|
||||
from worlds.alttp.Items import item_name_groups
|
||||
from worlds.alttp.Regions import lookup_vanilla_location_to_entrance
|
||||
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
|
||||
from worlds.alttp.Shops import ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
||||
from worlds.alttp.ItemPool import difficulties
|
||||
from worlds.alttp.Shops import SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
||||
from Utils import output_path, get_options, __version__, version_tuple
|
||||
from worlds.generic.Rules import locality_rules, exclusion_rules
|
||||
from worlds import AutoWorld
|
||||
@@ -30,15 +29,6 @@ def get_seed(seed=None):
|
||||
return seed
|
||||
|
||||
|
||||
def get_same_seed(world: MultiWorld, seed_def: tuple) -> str:
|
||||
seeds: Dict[tuple, str] = getattr(world, "__named_seeds", {})
|
||||
if seed_def in seeds:
|
||||
return seeds[seed_def]
|
||||
seeds[seed_def] = str(world.random.randint(0, 2 ** 64))
|
||||
world.__named_seeds = seeds
|
||||
return seeds[seed_def]
|
||||
|
||||
|
||||
def main(args, seed=None):
|
||||
if args.outputpath:
|
||||
os.makedirs(args.outputpath, exist_ok=True)
|
||||
@@ -76,11 +66,6 @@ def main(args, seed=None):
|
||||
world.retro = args.retro.copy()
|
||||
|
||||
world.hints = args.hints.copy()
|
||||
|
||||
world.mapshuffle = args.mapshuffle.copy()
|
||||
world.compassshuffle = args.compassshuffle.copy()
|
||||
world.keyshuffle = args.keyshuffle.copy()
|
||||
world.bigkeyshuffle = args.bigkeyshuffle.copy()
|
||||
world.open_pyramid = args.open_pyramid.copy()
|
||||
world.boss_shuffle = args.shufflebosses.copy()
|
||||
world.enemy_shuffle = args.enemy_shuffle.copy()
|
||||
@@ -97,7 +82,6 @@ def main(args, seed=None):
|
||||
world.green_clock_time = args.green_clock_time.copy()
|
||||
world.shufflepots = args.shufflepots.copy()
|
||||
world.dungeon_counters = args.dungeon_counters.copy()
|
||||
world.glitch_boots = args.glitch_boots.copy()
|
||||
world.triforce_pieces_available = args.triforce_pieces_available.copy()
|
||||
world.triforce_pieces_required = args.triforce_pieces_required.copy()
|
||||
world.shop_shuffle = args.shop_shuffle.copy()
|
||||
@@ -122,36 +106,23 @@ def main(args, seed=None):
|
||||
world.slot_seeds = {player: random.Random(world.random.getrandbits(64)) for player in
|
||||
range(1, world.players + 1)}
|
||||
|
||||
AutoWorld.call_all(world, "generate_early")
|
||||
|
||||
# system for sharing ER layouts
|
||||
for player in world.get_game_players("A Link to the Past"):
|
||||
world.er_seeds[player] = str(world.random.randint(0, 2 ** 64))
|
||||
|
||||
if "-" in world.shuffle[player]:
|
||||
shuffle, seed = world.shuffle[player].split("-", 1)
|
||||
world.shuffle[player] = shuffle
|
||||
if shuffle == "vanilla":
|
||||
world.er_seeds[player] = "vanilla"
|
||||
elif seed.startswith("group-") or args.race:
|
||||
world.er_seeds[player] = get_same_seed(world, (
|
||||
shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
|
||||
else: # not a race or group seed, use set seed as is.
|
||||
world.er_seeds[player] = seed
|
||||
elif world.shuffle[player] == "vanilla":
|
||||
world.er_seeds[player] = "vanilla"
|
||||
|
||||
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
|
||||
|
||||
logger.info("Found World Types:")
|
||||
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
|
||||
numlength = 8
|
||||
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
|
||||
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} Items | {len(cls.location_names):3} Locations")
|
||||
if not cls.hidden:
|
||||
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} Items | "
|
||||
f"{len(cls.location_names):3} Locations")
|
||||
logger.info(f" Item IDs: {min(cls.item_id_to_name):{numlength}} - "
|
||||
f"{max(cls.item_id_to_name):{numlength}} | "
|
||||
f"Location IDs: {min(cls.location_id_to_name):{numlength}} - "
|
||||
f"{max(cls.location_id_to_name):{numlength}}")
|
||||
|
||||
AutoWorld.call_all(world, "generate_early")
|
||||
|
||||
logger.info('')
|
||||
for player in world.get_game_players("A Link to the Past"):
|
||||
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
||||
|
||||
for player in world.player_ids:
|
||||
for item_name in args.startinventory[player]:
|
||||
@@ -163,21 +134,6 @@ def main(args, seed=None):
|
||||
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
|
||||
world.local_items[player].add('Triforce Piece')
|
||||
|
||||
# dungeon items can't be in non-local if the appropriate dungeon item shuffle setting is not set.
|
||||
if not world.mapshuffle[player]:
|
||||
world.non_local_items[player] -= item_name_groups['Maps']
|
||||
|
||||
if not world.compassshuffle[player]:
|
||||
world.non_local_items[player] -= item_name_groups['Compasses']
|
||||
|
||||
if not world.keyshuffle[player]:
|
||||
world.non_local_items[player] -= item_name_groups['Small Keys']
|
||||
# This could probably use a more elegant solution.
|
||||
elif world.keyshuffle[player] == True and world.mode[player] == "Standard":
|
||||
world.local_items[player].add("Small Key (Hyrule Castle)")
|
||||
if not world.bigkeyshuffle[player]:
|
||||
world.non_local_items[player] -= item_name_groups['Big Keys']
|
||||
|
||||
# Not possible to place pendants/crystals out side of boss prizes yet.
|
||||
world.non_local_items[player] -= item_name_groups['Pendants']
|
||||
world.non_local_items[player] -= item_name_groups['Crystals']
|
||||
@@ -221,181 +177,188 @@ def main(args, seed=None):
|
||||
elif world.algorithm == 'balanced':
|
||||
distribute_items_restrictive(world)
|
||||
|
||||
logger.info("Filling Shop Slots")
|
||||
|
||||
ShopSlotFill(world)
|
||||
AutoWorld.call_all(world, 'post_fill')
|
||||
|
||||
if world.players > 1:
|
||||
balance_multiworld_progression(world)
|
||||
|
||||
logger.info('Generating output files.')
|
||||
logger.info(f'Beginning output...')
|
||||
outfilebase = 'AP_' + world.seed_name
|
||||
|
||||
pool = concurrent.futures.ThreadPoolExecutor()
|
||||
|
||||
output = tempfile.TemporaryDirectory()
|
||||
with output as temp_dir:
|
||||
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
||||
|
||||
output_file_futures = []
|
||||
output_file_futures = []
|
||||
|
||||
for player in world.player_ids:
|
||||
output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
|
||||
output_file_futures.append(pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir))
|
||||
for player in world.player_ids:
|
||||
# skip starting a thread for methods that say "pass".
|
||||
if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__:
|
||||
output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
|
||||
output_file_futures.append(pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir))
|
||||
|
||||
def get_entrance_to_region(region: Region):
|
||||
for entrance in region.entrances:
|
||||
if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic):
|
||||
return entrance
|
||||
for entrance in region.entrances: # BFS might be better here, trying DFS for now.
|
||||
return get_entrance_to_region(entrance.parent_region)
|
||||
def get_entrance_to_region(region: Region):
|
||||
for entrance in region.entrances:
|
||||
if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic):
|
||||
return entrance
|
||||
for entrance in region.entrances: # BFS might be better here, trying DFS for now.
|
||||
return get_entrance_to_region(entrance.parent_region)
|
||||
|
||||
# collect ER hint info
|
||||
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
|
||||
world.shuffle[player] != "vanilla" or world.retro[player]}
|
||||
# collect ER hint info
|
||||
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
|
||||
world.shuffle[player] != "vanilla" or world.retro[player]}
|
||||
|
||||
for region in world.regions:
|
||||
if region.player in er_hint_data and region.locations:
|
||||
main_entrance = get_entrance_to_region(region)
|
||||
for location in region.locations:
|
||||
if type(location.address) == int: # skips events and crystals
|
||||
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
||||
er_hint_data[region.player][location.address] = main_entrance.name
|
||||
for region in world.regions:
|
||||
if region.player in er_hint_data and region.locations:
|
||||
main_entrance = get_entrance_to_region(region)
|
||||
for location in region.locations:
|
||||
if type(location.address) == int: # skips events and crystals
|
||||
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
||||
er_hint_data[region.player][location.address] = main_entrance.name
|
||||
|
||||
ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
|
||||
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
|
||||
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total")
|
||||
ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
|
||||
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
|
||||
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total")
|
||||
|
||||
checks_in_area = {player: {area: list() for area in ordered_areas}
|
||||
for player in range(1, world.players + 1)}
|
||||
checks_in_area = {player: {area: list() for area in ordered_areas}
|
||||
for player in range(1, world.players + 1)}
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
checks_in_area[player]["Total"] = 0
|
||||
for player in range(1, world.players + 1):
|
||||
checks_in_area[player]["Total"] = 0
|
||||
|
||||
for location in [loc for loc in world.get_filled_locations() if type(loc.address) is int]:
|
||||
main_entrance = get_entrance_to_region(location.parent_region)
|
||||
if location.game != "A Link to the Past":
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif location.parent_region.dungeon:
|
||||
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
|
||||
'Inverted Ganons Tower': 'Ganons Tower'} \
|
||||
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
||||
checks_in_area[location.player][dungeonname].append(location.address)
|
||||
elif main_entrance.parent_region.type == RegionType.LightWorld:
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif main_entrance.parent_region.type == RegionType.DarkWorld:
|
||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||
checks_in_area[location.player]["Total"] += 1
|
||||
|
||||
oldmancaves = []
|
||||
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
|
||||
for index, take_any in enumerate(takeanyregions):
|
||||
for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if
|
||||
world.retro[player]]:
|
||||
item = world.create_item(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
|
||||
region.player)
|
||||
player = region.player
|
||||
location_id = SHOP_ID_START + total_shop_slots + index
|
||||
|
||||
main_entrance = get_entrance_to_region(region)
|
||||
if main_entrance.parent_region.type == RegionType.LightWorld:
|
||||
checks_in_area[player]["Light World"].append(location_id)
|
||||
else:
|
||||
checks_in_area[player]["Dark World"].append(location_id)
|
||||
checks_in_area[player]["Total"] += 1
|
||||
|
||||
er_hint_data[player][location_id] = main_entrance.name
|
||||
oldmancaves.append(((location_id, player), (item.code, player)))
|
||||
|
||||
FillDisabledShopSlots(world)
|
||||
|
||||
def write_multidata():
|
||||
import NetUtils
|
||||
slot_data = {}
|
||||
client_versions = {}
|
||||
minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
|
||||
games = {}
|
||||
for slot in world.player_ids:
|
||||
client_versions[slot] = world.worlds[slot].get_required_client_version()
|
||||
games[slot] = world.game[slot]
|
||||
precollected_items = {player: [] for player in range(1, world.players + 1)}
|
||||
for item in world.precollected_items:
|
||||
precollected_items[item.player].append(item.code)
|
||||
precollected_hints = {player: set() for player in range(1, world.players + 1)}
|
||||
# for now special case Factorio tech_tree_information
|
||||
sending_visible_players = set()
|
||||
for player in world.get_game_players("Factorio"):
|
||||
if world.tech_tree_information[player].value == 2:
|
||||
sending_visible_players.add(player)
|
||||
|
||||
for slot in world.player_ids:
|
||||
slot_data[slot] = world.worlds[slot].fill_slot_data()
|
||||
|
||||
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
|
||||
for location in world.get_filled_locations():
|
||||
if type(location.address) == int:
|
||||
locations_data[location.player][location.address] = location.item.code, location.item.player
|
||||
if location.player in sending_visible_players and location.item.player != location.player:
|
||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||
location.item.code, False)
|
||||
precollected_hints[location.player].add(hint)
|
||||
precollected_hints[location.item.player].add(hint)
|
||||
elif location.item.name in args.start_hints[location.item.player]:
|
||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||
location.item.code, False,
|
||||
er_hint_data.get(location.player, {}).get(location.address, ""))
|
||||
precollected_hints[location.player].add(hint)
|
||||
precollected_hints[location.item.player].add(hint)
|
||||
if type(location.address) is int:
|
||||
main_entrance = get_entrance_to_region(location.parent_region)
|
||||
if location.game != "A Link to the Past":
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif location.parent_region.dungeon:
|
||||
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
|
||||
'Inverted Ganons Tower': 'Ganons Tower'} \
|
||||
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
||||
checks_in_area[location.player][dungeonname].append(location.address)
|
||||
elif main_entrance.parent_region.type == RegionType.LightWorld:
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif main_entrance.parent_region.type == RegionType.DarkWorld:
|
||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||
checks_in_area[location.player]["Total"] += 1
|
||||
|
||||
multidata = {
|
||||
"slot_data": slot_data,
|
||||
"games": games,
|
||||
"names": [[name for player, name in sorted(world.player_name.items())]],
|
||||
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
|
||||
"remote_items": {player for player in world.player_ids if
|
||||
world.worlds[player].remote_items},
|
||||
"locations": locations_data,
|
||||
"checks_in_area": checks_in_area,
|
||||
"server_options": get_options()["server_options"],
|
||||
"er_hint_data": er_hint_data,
|
||||
"precollected_items": precollected_items,
|
||||
"precollected_hints": precollected_hints,
|
||||
"version": tuple(version_tuple),
|
||||
"tags": ["AP"],
|
||||
"minimum_versions": minimum_versions,
|
||||
"seed_name": world.seed_name
|
||||
}
|
||||
AutoWorld.call_all(world, "modify_multidata", multidata)
|
||||
oldmancaves = []
|
||||
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
|
||||
for index, take_any in enumerate(takeanyregions):
|
||||
for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if
|
||||
world.retro[player]]:
|
||||
item = world.create_item(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
|
||||
region.player)
|
||||
player = region.player
|
||||
location_id = SHOP_ID_START + total_shop_slots + index
|
||||
|
||||
multidata = zlib.compress(pickle.dumps(multidata), 9)
|
||||
main_entrance = get_entrance_to_region(region)
|
||||
if main_entrance.parent_region.type == RegionType.LightWorld:
|
||||
checks_in_area[player]["Light World"].append(location_id)
|
||||
else:
|
||||
checks_in_area[player]["Dark World"].append(location_id)
|
||||
checks_in_area[player]["Total"] += 1
|
||||
|
||||
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:
|
||||
f.write(bytes([1])) # version of format
|
||||
f.write(multidata)
|
||||
er_hint_data[player][location_id] = main_entrance.name
|
||||
oldmancaves.append(((location_id, player), (item.code, player)))
|
||||
|
||||
FillDisabledShopSlots(world)
|
||||
|
||||
def write_multidata():
|
||||
import NetUtils
|
||||
slot_data = {}
|
||||
client_versions = {}
|
||||
minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
|
||||
games = {}
|
||||
for slot in world.player_ids:
|
||||
client_versions[slot] = world.worlds[slot].get_required_client_version()
|
||||
games[slot] = world.game[slot]
|
||||
precollected_items = {player: [] for player in range(1, world.players + 1)}
|
||||
for item in world.precollected_items:
|
||||
precollected_items[item.player].append(item.code)
|
||||
precollected_hints = {player: set() for player in range(1, world.players + 1)}
|
||||
# for now special case Factorio tech_tree_information
|
||||
sending_visible_players = set()
|
||||
for player in world.get_game_players("Factorio"):
|
||||
if world.tech_tree_information[player].value == 2:
|
||||
sending_visible_players.add(player)
|
||||
|
||||
for slot in world.player_ids:
|
||||
slot_data[slot] = world.worlds[slot].fill_slot_data()
|
||||
|
||||
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
|
||||
for location in world.get_filled_locations():
|
||||
if type(location.address) == int:
|
||||
# item code None should be event, location.address should then also be None
|
||||
assert location.item.code is not None
|
||||
locations_data[location.player][location.address] = location.item.code, location.item.player
|
||||
if location.player in sending_visible_players and location.item.player != location.player:
|
||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||
location.item.code, False)
|
||||
precollected_hints[location.player].add(hint)
|
||||
precollected_hints[location.item.player].add(hint)
|
||||
elif location.item.name in args.start_hints[location.item.player]:
|
||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||
location.item.code, False,
|
||||
er_hint_data.get(location.player, {}).get(location.address, ""))
|
||||
precollected_hints[location.player].add(hint)
|
||||
precollected_hints[location.item.player].add(hint)
|
||||
|
||||
multidata = {
|
||||
"slot_data": slot_data,
|
||||
"games": games,
|
||||
"names": [[name for player, name in sorted(world.player_name.items())]],
|
||||
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
|
||||
"remote_items": {player for player in world.player_ids if
|
||||
world.worlds[player].remote_items},
|
||||
"locations": locations_data,
|
||||
"checks_in_area": checks_in_area,
|
||||
"server_options": get_options()["server_options"],
|
||||
"er_hint_data": er_hint_data,
|
||||
"precollected_items": precollected_items,
|
||||
"precollected_hints": precollected_hints,
|
||||
"version": tuple(version_tuple),
|
||||
"tags": ["AP"],
|
||||
"minimum_versions": minimum_versions,
|
||||
"seed_name": world.seed_name
|
||||
}
|
||||
AutoWorld.call_all(world, "modify_multidata", multidata)
|
||||
|
||||
multidata = zlib.compress(pickle.dumps(multidata), 9)
|
||||
|
||||
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:
|
||||
f.write(bytes([1])) # version of format
|
||||
f.write(multidata)
|
||||
|
||||
multidata_task = pool.submit(write_multidata)
|
||||
if not check_accessibility_task.result():
|
||||
if not world.can_beat_game():
|
||||
raise Exception("Game appears as unbeatable. Aborting.")
|
||||
else:
|
||||
logger.warning("Location Accessibility requirements not fulfilled.")
|
||||
|
||||
# retrieve exceptions via .result() if they occured.
|
||||
if multidata_task:
|
||||
multidata_task.result()
|
||||
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures)):
|
||||
if i % 10 == 0:
|
||||
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')
|
||||
future.result()
|
||||
|
||||
multidata_task = pool.submit(write_multidata)
|
||||
if not check_accessibility_task.result():
|
||||
if not world.can_beat_game():
|
||||
raise Exception("Game appears as unbeatable. Aborting.")
|
||||
else:
|
||||
logger.warning("Location Accessibility requirements not fulfilled.")
|
||||
if multidata_task:
|
||||
multidata_task.result() # retrieve exception if one exists
|
||||
pool.shutdown() # wait for all queued tasks to complete
|
||||
if not args.skip_playthrough:
|
||||
logger.info('Calculating playthrough.')
|
||||
create_playthrough(world)
|
||||
|
||||
if args.create_spoiler:
|
||||
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
|
||||
for future in output_file_futures:
|
||||
future.result()
|
||||
|
||||
zipfilename = output_path(f"AP_{world.seed_name}.zip")
|
||||
logger.info(f'Creating final archive at {zipfilename}.')
|
||||
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
|
||||
compresslevel=9) as zf:
|
||||
for file in os.scandir(temp_dir):
|
||||
zf.write(os.path.join(temp_dir, file), arcname=file.name)
|
||||
zf.write(file.path, arcname=file.name)
|
||||
|
||||
logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
|
||||
return world
|
||||
@@ -411,7 +374,6 @@ def create_playthrough(world):
|
||||
sphere_candidates = set(prog_locations)
|
||||
logging.debug('Building up collection spheres.')
|
||||
while sphere_candidates:
|
||||
state.sweep_for_events(key_only=True)
|
||||
|
||||
# build up spheres of collection radius.
|
||||
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
||||
@@ -516,14 +478,15 @@ def create_playthrough(world):
|
||||
{str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in
|
||||
sphere if location.player == player})
|
||||
if player in world.get_game_players("A Link to the Past"):
|
||||
for path in dict(world.spoiler.paths).values():
|
||||
if any(exit_path == 'Pyramid Fairy' for (_, exit_path) in path):
|
||||
if world.mode[player] != 'inverted':
|
||||
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = \
|
||||
get_path(state,world.get_region('Big Bomb Shop', player))
|
||||
else:
|
||||
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = \
|
||||
get_path(state,world.get_region('Inverted Big Bomb Shop', player))
|
||||
# If Pyramid Fairy Entrance needs to be reached, also path to Big Bomb Shop
|
||||
# Maybe move the big bomb over to the Event system instead?
|
||||
if any(exit_path == 'Pyramid Fairy' for path in world.spoiler.paths.values() for (_, exit_path) in path):
|
||||
if world.mode[player] != 'inverted':
|
||||
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = \
|
||||
get_path(state, world.get_region('Big Bomb Shop', player))
|
||||
else:
|
||||
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = \
|
||||
get_path(state, world.get_region('Inverted Big Bomb Shop', player))
|
||||
|
||||
# we can finally output our playthrough
|
||||
world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])}
|
||||
|
||||
@@ -61,15 +61,20 @@ def replace_apmc_files(forge_dir, apmc_file):
|
||||
if apmc_file is None:
|
||||
return
|
||||
apdata_dir = os.path.join(forge_dir, 'APData')
|
||||
copy_apmc = True
|
||||
if not os.path.isdir(apdata_dir):
|
||||
os.mkdir(apdata_dir)
|
||||
print(f"Created APData folder in {forge_dir}")
|
||||
for entry in os.scandir(apdata_dir):
|
||||
if ".apmc" in entry.name and entry.is_file():
|
||||
os.remove(entry.path)
|
||||
print(f"Removed {entry.name} in {apdata_dir}")
|
||||
copyfile(apmc_file, os.path.join(apdata_dir, os.path.basename(apmc_file)))
|
||||
print(f"Copied {os.path.basename(apmc_file)} to {apdata_dir}")
|
||||
if entry.name.endswith(".apmc") and entry.is_file():
|
||||
if not os.path.samefile(apmc_file, entry.path):
|
||||
os.remove(entry.path)
|
||||
print(f"Removed {entry.name} in {apdata_dir}")
|
||||
else: # apmc already in apdata
|
||||
copy_apmc = False
|
||||
if copy_apmc:
|
||||
copyfile(apmc_file, os.path.join(apdata_dir, os.path.basename(apmc_file)))
|
||||
print(f"Copied {os.path.basename(apmc_file)} to {apdata_dir}")
|
||||
|
||||
|
||||
# Check mod version, download new mod from GitHub releases page if needed.
|
||||
@@ -157,14 +162,14 @@ if __name__ == '__main__':
|
||||
parser.add_argument("apmc_file", default=None, nargs='?', help="Path to an Archipelago Minecraft data file (.apmc)")
|
||||
|
||||
args = parser.parse_args()
|
||||
options = Utils.get_options()
|
||||
|
||||
apmc_file = os.path.abspath(args.apmc_file)
|
||||
forge_dir = options["minecraft_options"]["forge_directory"]
|
||||
max_heap = options["minecraft_options"]["max_heap_size"]
|
||||
apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None
|
||||
|
||||
# Change to executable's working directory
|
||||
os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
|
||||
|
||||
options = Utils.get_options()
|
||||
forge_dir = options["minecraft_options"]["forge_directory"]
|
||||
max_heap = options["minecraft_options"]["max_heap_size"]
|
||||
|
||||
if apmc_file is not None and not os.path.isfile(apmc_file):
|
||||
raise FileNotFoundError(f"Path {apmc_file} does not exist or could not be accessed.")
|
||||
|
||||
185
MultiServer.py
185
MultiServer.py
@@ -31,10 +31,11 @@ from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_
|
||||
import Utils
|
||||
from Utils import get_item_name_from_id, get_location_name_from_id, \
|
||||
version_tuple, restricted_loads, Version
|
||||
from NetUtils import Node, Endpoint, ClientStatus, NetworkItem, decode, NetworkPlayer
|
||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer
|
||||
|
||||
colorama.init()
|
||||
|
||||
|
||||
class Client(Endpoint):
|
||||
version = Version(0, 0, 0)
|
||||
tags: typing.List[str] = []
|
||||
@@ -50,9 +51,14 @@ class Client(Endpoint):
|
||||
self.messageprocessor = client_message_processor(ctx, self)
|
||||
self.ctx = weakref.ref(ctx)
|
||||
|
||||
|
||||
team_slot = typing.Tuple[int, int]
|
||||
|
||||
class Context(Node):
|
||||
|
||||
class Context:
|
||||
dumper = staticmethod(encode)
|
||||
loader = staticmethod(decode)
|
||||
|
||||
simple_options = {"hint_cost": int,
|
||||
"location_check_points": int,
|
||||
"server_password": str,
|
||||
@@ -64,8 +70,10 @@ class Context(Node):
|
||||
|
||||
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", remaining_mode: str = "disabled",
|
||||
auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2):
|
||||
auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, log_network: bool = False):
|
||||
super(Context, self).__init__()
|
||||
self.log_network = log_network
|
||||
self.endpoints = []
|
||||
self.compatibility: int = compatibility
|
||||
self.shutdown_task = None
|
||||
self.data_filename = None
|
||||
@@ -113,10 +121,70 @@ class Context(Node):
|
||||
self.seed_name = ""
|
||||
self.random = random.Random()
|
||||
|
||||
def get_hint_cost(self, slot):
|
||||
if self.hint_cost:
|
||||
return max(0, int(self.hint_cost * 0.01 * len(self.locations[slot])))
|
||||
return 0
|
||||
# General networking
|
||||
|
||||
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
|
||||
if not endpoint.socket or not endpoint.socket.open:
|
||||
return False
|
||||
msg = self.dumper(msgs)
|
||||
try:
|
||||
await endpoint.socket.send(msg)
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception(f"Exception during send_msgs, could not send {msg}")
|
||||
await self.disconnect(endpoint)
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
return True
|
||||
|
||||
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool:
|
||||
if not endpoint.socket or not endpoint.socket.open:
|
||||
return False
|
||||
try:
|
||||
await endpoint.socket.send(msg)
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception("Exception during send_encoded_msgs")
|
||||
await self.disconnect(endpoint)
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
return True
|
||||
|
||||
def broadcast_all(self, msgs):
|
||||
msgs = self.dumper(msgs)
|
||||
for endpoint in self.endpoints:
|
||||
if endpoint.auth:
|
||||
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
|
||||
|
||||
def broadcast_team(self, team, msgs):
|
||||
msgs = self.dumper(msgs)
|
||||
for client in self.endpoints:
|
||||
if client.auth and client.team == team:
|
||||
asyncio.create_task(self.send_encoded_msgs(client, msgs))
|
||||
|
||||
async def disconnect(self, endpoint):
|
||||
if endpoint in self.endpoints:
|
||||
self.endpoints.remove(endpoint)
|
||||
await on_client_disconnected(self, endpoint)
|
||||
|
||||
# text
|
||||
|
||||
def notify_all(self, text):
|
||||
logging.info("Notice (all): %s" % text)
|
||||
self.broadcast_all([{"cmd": "Print", "text": text}])
|
||||
|
||||
def notify_client(self, client: Client, text: str):
|
||||
if not client.auth:
|
||||
return
|
||||
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
||||
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}]))
|
||||
|
||||
def notify_client_multiple(self, client: Client, texts: typing.List[str]):
|
||||
if not client.auth:
|
||||
return
|
||||
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts]))
|
||||
|
||||
# loading
|
||||
|
||||
def load(self, multidatapath: str, use_embedded_server_options: bool = False):
|
||||
if multidatapath.lower().endswith(".zip"):
|
||||
@@ -177,27 +245,7 @@ class Context(Node):
|
||||
server_options = decoded_obj.get("server_options", {})
|
||||
self._set_options(server_options)
|
||||
|
||||
def get_players_package(self):
|
||||
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
|
||||
|
||||
def _set_options(self, server_options: dict):
|
||||
for key, value in server_options.items():
|
||||
data_type = self.simple_options.get(key, None)
|
||||
if data_type is not None:
|
||||
if value not in {False, True, None}: # some can be boolean OR text, such as password
|
||||
try:
|
||||
value = data_type(value)
|
||||
except Exception as e:
|
||||
try:
|
||||
raise Exception(f"Could not set server option {key}, skipping.") from e
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
|
||||
setattr(self, key, value)
|
||||
elif key == "disable_item_cheat":
|
||||
self.item_cheat = not bool(value)
|
||||
else:
|
||||
logging.debug(f"Unrecognized server option {key}")
|
||||
# saving
|
||||
|
||||
def save(self, now=False) -> bool:
|
||||
if self.saving:
|
||||
@@ -228,7 +276,7 @@ class Context(Node):
|
||||
import os
|
||||
name, ext = os.path.splitext(self.data_filename)
|
||||
self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago','.zip') \
|
||||
else self.data_filename + '_' + 'apsave'
|
||||
else self.data_filename + '_' + 'apsave'
|
||||
try:
|
||||
with open(self.save_filename, 'rb') as f:
|
||||
save_data = restricted_loads(zlib.decompress(f.read()))
|
||||
@@ -256,13 +304,6 @@ class Context(Node):
|
||||
import atexit
|
||||
atexit.register(self._save, True) # make sure we save on exit too
|
||||
|
||||
def recheck_hints(self):
|
||||
for team, slot in self.hints:
|
||||
self.hints[team, slot] = {
|
||||
hint.re_check(self, team) for hint in
|
||||
self.hints[team, slot]
|
||||
}
|
||||
|
||||
def get_save(self) -> dict:
|
||||
self.recheck_hints()
|
||||
d = {
|
||||
@@ -303,43 +344,48 @@ class Context(Node):
|
||||
logging.info(f'Loaded save file with {sum([len(p) for p in self.received_items.values()])} received items '
|
||||
f'for {len(self.received_items)} players')
|
||||
|
||||
# rest
|
||||
|
||||
def get_hint_cost(self, slot):
|
||||
if self.hint_cost:
|
||||
return max(0, int(self.hint_cost * 0.01 * len(self.locations[slot])))
|
||||
return 0
|
||||
|
||||
def recheck_hints(self):
|
||||
for team, slot in self.hints:
|
||||
self.hints[team, slot] = {
|
||||
hint.re_check(self, team) for hint in
|
||||
self.hints[team, slot]
|
||||
}
|
||||
|
||||
def get_players_package(self):
|
||||
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
|
||||
|
||||
def _set_options(self, server_options: dict):
|
||||
for key, value in server_options.items():
|
||||
data_type = self.simple_options.get(key, None)
|
||||
if data_type is not None:
|
||||
if value not in {False, True, None}: # some can be boolean OR text, such as password
|
||||
try:
|
||||
value = data_type(value)
|
||||
except Exception as e:
|
||||
try:
|
||||
raise Exception(f"Could not set server option {key}, skipping.") from e
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
|
||||
setattr(self, key, value)
|
||||
elif key == "disable_item_cheat":
|
||||
self.item_cheat = not bool(value)
|
||||
else:
|
||||
logging.debug(f"Unrecognized server option {key}")
|
||||
|
||||
def get_aliased_name(self, team: int, slot: int):
|
||||
if (team, slot) in self.name_aliases:
|
||||
return f"{self.name_aliases[team, slot]} ({self.player_names[team, slot]})"
|
||||
else:
|
||||
return self.player_names[team, slot]
|
||||
|
||||
def notify_all(self, text):
|
||||
logging.info("Notice (all): %s" % text)
|
||||
self.broadcast_all([{"cmd": "Print", "text": text}])
|
||||
|
||||
def notify_client(self, client: Client, text: str):
|
||||
if not client.auth:
|
||||
return
|
||||
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
||||
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}]))
|
||||
|
||||
def notify_client_multiple(self, client: Client, texts: typing.List[str]):
|
||||
if not client.auth:
|
||||
return
|
||||
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts]))
|
||||
|
||||
def broadcast_team(self, team, msgs):
|
||||
msgs = self.dumper(msgs)
|
||||
for client in self.endpoints:
|
||||
if client.auth and client.team == team:
|
||||
asyncio.create_task(self.send_encoded_msgs(client, msgs))
|
||||
|
||||
def broadcast_all(self, msgs):
|
||||
msgs = self.dumper(msgs)
|
||||
for endpoint in self.endpoints:
|
||||
if endpoint.auth:
|
||||
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
|
||||
|
||||
async def disconnect(self, endpoint):
|
||||
await super(Context, self).disconnect(endpoint)
|
||||
await on_client_disconnected(self, endpoint)
|
||||
|
||||
|
||||
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint]):
|
||||
concerns = collections.defaultdict(list)
|
||||
@@ -1147,6 +1193,8 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus)
|
||||
ctx.notify_all(finished_msg)
|
||||
if "auto" in ctx.forfeit_mode:
|
||||
forfeit_player(ctx, client.team, client.slot)
|
||||
elif proxy_worlds[ctx.games[client.slot]].forced_auto_forfeit:
|
||||
forfeit_player(ctx, client.team, client.slot)
|
||||
|
||||
ctx.client_game_state[client.team, client.slot] = new_status
|
||||
|
||||
@@ -1431,8 +1479,7 @@ async def main(args: argparse.Namespace):
|
||||
|
||||
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
|
||||
args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.remaining_mode,
|
||||
args.auto_shutdown, args.compatibility)
|
||||
ctx.log_network = args.log_network
|
||||
args.auto_shutdown, args.compatibility, args.log_network)
|
||||
data_filename = args.multidata
|
||||
|
||||
try:
|
||||
|
||||
48
NetUtils.py
48
NetUtils.py
@@ -1,6 +1,4 @@
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
import typing
|
||||
import enum
|
||||
from json import JSONEncoder, JSONDecoder
|
||||
@@ -94,52 +92,6 @@ def _object_hook(o: typing.Any) -> typing.Any:
|
||||
decode = JSONDecoder(object_hook=_object_hook).decode
|
||||
|
||||
|
||||
class Node:
|
||||
endpoints: typing.List
|
||||
dumper = staticmethod(encode)
|
||||
loader = staticmethod(decode)
|
||||
|
||||
def __init__(self):
|
||||
self.endpoints = []
|
||||
super(Node, self).__init__()
|
||||
self.log_network = 0
|
||||
|
||||
def broadcast_all(self, msgs):
|
||||
msgs = self.dumper(msgs)
|
||||
for endpoint in self.endpoints:
|
||||
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
|
||||
|
||||
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
|
||||
if not endpoint.socket or not endpoint.socket.open:
|
||||
return False
|
||||
msg = self.dumper(msgs)
|
||||
try:
|
||||
await endpoint.socket.send(msg)
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception(f"Exception during send_msgs, could not send {msg}")
|
||||
await self.disconnect(endpoint)
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
return True
|
||||
|
||||
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool:
|
||||
if not endpoint.socket or not endpoint.socket.open:
|
||||
return False
|
||||
try:
|
||||
await endpoint.socket.send(msg)
|
||||
except websockets.ConnectionClosed:
|
||||
logging.exception("Exception during send_encoded_msgs")
|
||||
await self.disconnect(endpoint)
|
||||
else:
|
||||
if self.log_network:
|
||||
logging.info(f"Outgoing message: {msg}")
|
||||
return True
|
||||
|
||||
async def disconnect(self, endpoint):
|
||||
if endpoint in self.endpoints:
|
||||
self.endpoints.remove(endpoint)
|
||||
|
||||
|
||||
class Endpoint:
|
||||
socket: websockets.WebSocketServerProtocol
|
||||
|
||||
70
Options.py
70
Options.py
@@ -9,7 +9,7 @@ class AssembleOptions(type):
|
||||
name_lookup = attrs["name_lookup"] = {}
|
||||
# merge parent class options
|
||||
for base in bases:
|
||||
if hasattr(base, "options"):
|
||||
if getattr(base, "options", None):
|
||||
options.update(base.options)
|
||||
name_lookup.update(base.name_lookup)
|
||||
new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if
|
||||
@@ -57,11 +57,12 @@ class Option(metaclass=AssembleOptions):
|
||||
"""For display purposes."""
|
||||
return self.get_option_name(self.value)
|
||||
|
||||
def get_option_name(self, value: typing.Any) -> str:
|
||||
if self.autodisplayname:
|
||||
return self.name_lookup[self.value].replace("_", " ").title()
|
||||
@classmethod
|
||||
def get_option_name(cls, value: typing.Any) -> str:
|
||||
if cls.autodisplayname:
|
||||
return cls.name_lookup[value].replace("_", " ").title()
|
||||
else:
|
||||
return self.name_lookup[self.value]
|
||||
return cls.name_lookup[value]
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.value
|
||||
@@ -114,7 +115,8 @@ class Toggle(Option):
|
||||
def __int__(self):
|
||||
return int(self.value)
|
||||
|
||||
def get_option_name(self, value):
|
||||
@classmethod
|
||||
def get_option_name(cls, value):
|
||||
return ["No", "Yes"][int(value)]
|
||||
|
||||
class DefaultOnToggle(Toggle):
|
||||
@@ -147,6 +149,29 @@ class Choice(Option):
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, str):
|
||||
assert other in self.options
|
||||
return other == self.current_key
|
||||
elif isinstance(other, int):
|
||||
assert other in self.name_lookup
|
||||
return other == self.value
|
||||
elif isinstance(other, bool):
|
||||
return other == bool(self.value)
|
||||
else:
|
||||
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
||||
def __ne__(self, other):
|
||||
if isinstance(other, str):
|
||||
assert other in self.options
|
||||
return other != self.current_key
|
||||
elif isinstance(other, int):
|
||||
assert other in self.name_lookup
|
||||
return other != self.value
|
||||
elif isinstance(other, bool):
|
||||
return other != bool(self.value)
|
||||
else:
|
||||
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
||||
class Range(Option, int):
|
||||
range_start = 0
|
||||
@@ -220,6 +245,27 @@ class OptionDict(Option):
|
||||
return str(value)
|
||||
|
||||
|
||||
class OptionList(Option):
|
||||
default = []
|
||||
|
||||
def __init__(self, value: typing.List[str, typing.Any]):
|
||||
self.value = value
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str):
|
||||
return cls([option.strip() for option in text.split(",")])
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any):
|
||||
if type(data) == list:
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
def get_option_name(self, value):
|
||||
return str(value)
|
||||
|
||||
|
||||
|
||||
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
|
||||
|
||||
|
||||
@@ -233,14 +279,14 @@ if __name__ == "__main__":
|
||||
|
||||
from worlds.alttp.Options import Logic
|
||||
import argparse
|
||||
mapshuffle = Toggle
|
||||
compassshuffle = Toggle
|
||||
map_shuffle = Toggle
|
||||
compass_shuffle = Toggle
|
||||
keyshuffle = Toggle
|
||||
bigkeyshuffle = Toggle
|
||||
bigkey_shuffle = Toggle
|
||||
hints = Toggle
|
||||
test = argparse.Namespace()
|
||||
test.logic = Logic.from_text("no_logic")
|
||||
test.mapshuffle = mapshuffle.from_text("ON")
|
||||
test.map_shuffle = map_shuffle.from_text("ON")
|
||||
test.hints = hints.from_text('OFF')
|
||||
try:
|
||||
test.logic = Logic.from_text("overworld_glitches_typo")
|
||||
@@ -250,7 +296,7 @@ if __name__ == "__main__":
|
||||
test.logic_owg = Logic.from_text("owg")
|
||||
except KeyError as e:
|
||||
print(e)
|
||||
if test.mapshuffle:
|
||||
print("Mapshuffle is on")
|
||||
if test.map_shuffle:
|
||||
print("map_shuffle is on")
|
||||
print(f"Hints are {bool(test.hints)}")
|
||||
print(test)
|
||||
|
||||
@@ -7,8 +7,11 @@ Currently, the following games are supported:
|
||||
* Factorio
|
||||
* Minecraft
|
||||
* Subnautica
|
||||
* Slay the Spire
|
||||
* Risk of Rain 2
|
||||
* The Legend of Zelda: Ocarina of Time
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial).
|
||||
For setup and instructions check out our [tutorials page](http://archipelago.gg:48484/tutorial).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
windows binaries.
|
||||
|
||||
@@ -37,6 +40,7 @@ This project makes use of multiple other projects. We wouldn't be here without t
|
||||
|
||||
* [z3randomizer](https://github.com/CaitSith2/z3randomizer)
|
||||
* [Enemizer](https://github.com/Ijwu/Enemizer)
|
||||
* [Ocarina of Time Randomizer](https://github.com/TestRunnerSRL/OoT-Randomizer)
|
||||
|
||||
## Contributing
|
||||
Contributions are welcome. We have a few asks of any new contributors.
|
||||
|
||||
13
Utils.py
13
Utils.py
@@ -13,7 +13,7 @@ class Version(typing.NamedTuple):
|
||||
build: int
|
||||
|
||||
|
||||
__version__ = "0.1.6"
|
||||
__version__ = "0.1.7"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
import builtins
|
||||
@@ -196,6 +196,13 @@ def get_default_options() -> dict:
|
||||
"glitch_triforce_room": 1,
|
||||
"race": 0,
|
||||
"plando_options": "bosses",
|
||||
},
|
||||
"minecraft_options": {
|
||||
"forge_directory": "Minecraft Forge server",
|
||||
"max_heap_size": "2G"
|
||||
},
|
||||
"oot_options": {
|
||||
"rom_file": "The Legend of Zelda - Ocarina of Time.z64",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +285,7 @@ def persistent_load() -> typing.Dict[dict]:
|
||||
return storage
|
||||
|
||||
|
||||
def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
|
||||
def get_adjuster_settings(romfile: str, skip_questions: bool = False) -> typing.Tuple[str, bool]:
|
||||
if hasattr(get_adjuster_settings, "adjuster_settings"):
|
||||
adjuster_settings = getattr(get_adjuster_settings, "adjuster_settings")
|
||||
else:
|
||||
@@ -308,6 +315,8 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
|
||||
adjust_wanted = getattr(get_adjuster_settings, "adjust_wanted")
|
||||
elif persistent_load().get("adjuster", {}).get("never_adjust", False): # never adjust, per user request
|
||||
return romfile, False
|
||||
elif skip_questions:
|
||||
return romfile, False
|
||||
else:
|
||||
adjust_wanted = input(f"Last used adjuster settings were found. Would you like to apply these? \n"
|
||||
f"{pprint.pformat(printed_options)}\n"
|
||||
|
||||
@@ -8,6 +8,7 @@ from pony.flask import Pony
|
||||
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
||||
from flask_caching import Cache
|
||||
from flask_compress import Compress
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
from .models import *
|
||||
|
||||
@@ -81,34 +82,6 @@ def page_not_found(err):
|
||||
return render_template('404.html'), 404
|
||||
|
||||
|
||||
games_list = {
|
||||
"A Link to the Past": ("The Legend of Zelda: A Link to the Past",
|
||||
"""
|
||||
The Legend of Zelda: A Link to the Past is an action/adventure game. Take on the role of
|
||||
Link, a boy who is destined to save the land of Hyrule. Delve through three palaces and nine
|
||||
dungeons on your quest to rescue the descendents of the seven wise men and defeat the evil
|
||||
Ganon!"""),
|
||||
"Factorio": ("Factorio",
|
||||
"""
|
||||
Factorio is a game about automation. You play as an engineer who has crash landed on the planet
|
||||
Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory,
|
||||
research new technologies, and become more efficient in your quest to build a rocket and return home.
|
||||
"""),
|
||||
"Minecraft": ("Minecraft",
|
||||
"""
|
||||
Minecraft is a game about creativity. In a world made entirely of cubes, you explore, discover, mine,
|
||||
craft, and try not to explode. Delve deep into the earth and discover abandoned mines, ancient
|
||||
structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim
|
||||
victory!"""),
|
||||
"Subnautica": ("Subnautica",
|
||||
"""
|
||||
Subnautica is an undersea exploration game. Stranded on an alien world, you become infected by
|
||||
an unknown bacteria. The planet's automatic quarantine will shoot you down if you try to leave.
|
||||
You must find a cure for yourself, build an escape rocket, and leave the planet.
|
||||
"""),
|
||||
}
|
||||
|
||||
|
||||
# Player settings pages
|
||||
@app.route('/games/<string:game>/player-settings')
|
||||
def player_settings(game):
|
||||
@@ -130,7 +103,11 @@ def game_page(game):
|
||||
# List of supported games
|
||||
@app.route('/games')
|
||||
def games():
|
||||
return render_template("games/games.html", games_list=games_list)
|
||||
worlds = {}
|
||||
for game, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden:
|
||||
worlds[game] = world.__doc__ if world.__doc__ else "No description provided."
|
||||
return render_template("games/games.html", worlds=worlds)
|
||||
|
||||
|
||||
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
||||
@@ -138,14 +115,14 @@ def tutorial(game, file, lang):
|
||||
return render_template("tutorial.html", game=game, file=file, lang=lang)
|
||||
|
||||
|
||||
@app.route('/tutorial')
|
||||
@app.route('/tutorial/')
|
||||
def tutorial_landing():
|
||||
return render_template("tutorialLanding.html")
|
||||
|
||||
|
||||
@app.route('/weighted-settings')
|
||||
def weighted_settings():
|
||||
return render_template("weightedSettings.html")
|
||||
@app.route('/faq/<string:lang>/')
|
||||
def faq(lang):
|
||||
return render_template("faq.html", lang=lang)
|
||||
|
||||
|
||||
@app.route('/seed/<suuid:seed>')
|
||||
@@ -198,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'),
|
||||
|
||||
@@ -93,10 +93,10 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
||||
"sid": generation.id,
|
||||
"owner": generation.owner},
|
||||
handle_generation_success, handle_generation_failure)
|
||||
except:
|
||||
except Exception as e:
|
||||
generation.state = STATE_ERROR
|
||||
commit()
|
||||
raise
|
||||
logging.exception(e)
|
||||
else:
|
||||
generation.state = STATE_STARTED
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from flask import send_file, Response
|
||||
from flask import send_file, Response, render_template
|
||||
from pony.orm import select
|
||||
|
||||
from Patch import update_patch_data
|
||||
from WebHostLib import app, Slot, Room, Seed
|
||||
from WebHostLib import app, Slot, Room, Seed, cache
|
||||
import zipfile
|
||||
|
||||
@app.route("/dl_patch/<suuid:room_id>/<int:patch_id>")
|
||||
@@ -66,6 +66,18 @@ def download_slot_file(room_id, player_id: int):
|
||||
for name in zf.namelist():
|
||||
if name.endswith("info.json"):
|
||||
fname = name.rsplit("/", 1)[0]+".zip"
|
||||
elif slot_data.game == "Ocarina of Time":
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apz5"
|
||||
else:
|
||||
return "Game download not supported."
|
||||
return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname)
|
||||
return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname)
|
||||
|
||||
@app.route("/templates")
|
||||
@cache.cached()
|
||||
def list_yaml_templates():
|
||||
files = []
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
for world_name, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden:
|
||||
files.append(world_name)
|
||||
return render_template("templates.html", files=files)
|
||||
@@ -10,9 +10,18 @@ target_folder = os.path.join("WebHostLib", "static", "generated")
|
||||
|
||||
|
||||
def create():
|
||||
def dictify_range(option):
|
||||
data = {option.range_start: 0, option.range_end: 0, "random": 0, "random-low": 0, "random-high": 0,
|
||||
option.default: 50}
|
||||
notes = {
|
||||
option.range_start: "minimum value",
|
||||
option.range_end: "maximum value"
|
||||
}
|
||||
return data, notes
|
||||
for game_name, world in AutoWorldRegister.world_types.items():
|
||||
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
|
||||
options=world.options, __version__=__version__, game=game_name, yaml_dump=yaml.dump
|
||||
options=world.options, __version__=__version__, game=game_name, yaml_dump=yaml.dump,
|
||||
dictify_range=dictify_range
|
||||
)
|
||||
|
||||
with open(os.path.join(target_folder, game_name + ".yaml"), "w") as f:
|
||||
@@ -31,7 +40,7 @@ def create():
|
||||
if option.options:
|
||||
this_option = {
|
||||
"type": "select",
|
||||
"friendlyName": option.friendly_name if hasattr(option, "friendly_name") else option_name,
|
||||
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
"defaultValue": None,
|
||||
"options": []
|
||||
@@ -39,7 +48,7 @@ def create():
|
||||
|
||||
for sub_option_name, sub_option_id in option.options.items():
|
||||
this_option["options"].append({
|
||||
"name": sub_option_name,
|
||||
"name": option.get_option_name(sub_option_id),
|
||||
"value": sub_option_name,
|
||||
})
|
||||
|
||||
@@ -51,7 +60,7 @@ def create():
|
||||
elif hasattr(option, "range_start") and hasattr(option, "range_end"):
|
||||
game_options[option_name] = {
|
||||
"type": "range",
|
||||
"friendlyName": option.friendly_name if hasattr(option, "friendly_name") else option_name,
|
||||
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
"defaultValue": option.default if hasattr(option, "default") else option.range_start,
|
||||
"min": option.range_start,
|
||||
|
||||
50
WebHostLib/static/assets/faq.js
Normal file
50
WebHostLib/static/assets/faq.js
Normal file
@@ -0,0 +1,50 @@
|
||||
window.addEventListener('load', () => {
|
||||
const tutorialWrapper = document.getElementById('faq-wrapper');
|
||||
new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status === 404) {
|
||||
reject("Sorry, the tutorial is not available in that language yet.");
|
||||
return;
|
||||
}
|
||||
if (ajax.status !== 200) {
|
||||
reject("Something went wrong while loading the tutorial.");
|
||||
return;
|
||||
}
|
||||
resolve(ajax.responseText);
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/assets/faq/` +
|
||||
`faq_${tutorialWrapper.getAttribute('data-lang')}.md`, true);
|
||||
ajax.send();
|
||||
}).then((results) => {
|
||||
// Populate page with HTML generated from markdown
|
||||
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
// Reset the id of all header divs to something nicer
|
||||
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
|
||||
const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/);
|
||||
for (let i=0; i < headers.length; i++){
|
||||
const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase()
|
||||
headers[i].setAttribute('id', headerId);
|
||||
headers[i].addEventListener('click', () =>
|
||||
window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`);
|
||||
}
|
||||
|
||||
// Manually scroll the user to the appropriate header if anchor navigation is used
|
||||
if (scrollTargetIndex > -1) {
|
||||
try{
|
||||
const scrollTarget = window.location.href.substring(scrollTargetIndex + 1);
|
||||
document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" });
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
tutorialWrapper.innerHTML =
|
||||
`<h2>This page is out of logic!</h2>
|
||||
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
|
||||
});
|
||||
});
|
||||
10
WebHostLib/static/assets/faq/faq_en.md
Normal file
10
WebHostLib/static/assets/faq/faq_en.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
## What is a randomizer?
|
||||
Who's on first.
|
||||
|
||||
## What is a multi-world?
|
||||
What's on second.
|
||||
|
||||
## What does multi-game mean?
|
||||
I don't know's on third.
|
||||
@@ -83,7 +83,7 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
||||
const label = document.createElement('label');
|
||||
label.setAttribute('for', setting);
|
||||
label.setAttribute('data-tooltip', settings[setting].description);
|
||||
label.innerText = `${settings[setting].friendlyName}:`;
|
||||
label.innerText = `${settings[setting].displayName}:`;
|
||||
tdl.appendChild(label);
|
||||
tr.appendChild(tdl);
|
||||
|
||||
|
||||
69
WebHostLib/static/assets/tutorial/ror2/setup_en.md
Normal file
69
WebHostLib/static/assets/tutorial/ror2/setup_en.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Risk of Rain 2 Setup Guide
|
||||
|
||||
## Install using r2modman
|
||||
### Install r2modman
|
||||
Head on over to the r2modman page on Thunderstore and follow the installation instructions.
|
||||
|
||||
https://thunderstore.io/package/ebkr/r2modman/
|
||||
|
||||
### Install Archipelago Mod using r2modman
|
||||
You can install the Archipelago mod using r2modman in one of two ways.
|
||||
One, you can use the Thunderstore website and click on the "Install with Mod Manager" link.
|
||||
|
||||
https://thunderstore.io/package/ArchipelagoMW/Archipelago/
|
||||
|
||||
You can also search for the "Archipelago" mod in the r2modman interface.
|
||||
The mod manager should automatically install all necessary dependencies as well.
|
||||
|
||||
### Running the Modded Game
|
||||
Click on the "Start modded" button in the top left in r2modman to start the game with the
|
||||
Archipelago mod installed.
|
||||
|
||||
## Joining an Archipelago Session
|
||||
There will be a menu button on the right side of the screen in the character select menu.
|
||||
Click it in order to bring up the in lobby mod config.
|
||||
From here you can expand the Archipelago sections and fill in the relevant info.
|
||||
Keep password blank if there is no password on the server.
|
||||
|
||||
Simply check `Enable Archipelago?` and when you start the run it will automatically connect.
|
||||
|
||||
## Gameplay
|
||||
The Risk of Rain 2 players send checks by causing items to spawn in-game. That means opening chests or killing bosses, generally.
|
||||
An item check is only sent out after a certain number of items are picked up. This count is configurable in the player's YAML.
|
||||
|
||||
## YAML Settings
|
||||
An example YAML would look like this:
|
||||
```yaml
|
||||
description: Ijwu-ror2
|
||||
name: Ijwu
|
||||
|
||||
game:
|
||||
Risk of Rain 2: 1
|
||||
|
||||
Risk of Rain 2:
|
||||
total_locations: 15
|
||||
total_revivals: 4
|
||||
start_with_revive: true
|
||||
item_pickup_step: 1
|
||||
enable_lunar: true
|
||||
```
|
||||
|
||||
| Name | Description | Allowed values |
|
||||
| ---- | ----------- | -------------- |
|
||||
| total_locations | The total number of location checks that will be attributed to the Risk of Rain player. This option is ALSO the total number of items in the item pool for the Risk of Rain player. | 10 - 50 |
|
||||
| total_revivals | The total number of items in the Risk of Rain player's item pool (items other players pick up for them) replaced with `Dio's Best Friend`. | 0 - 5 |
|
||||
| start_with_revive | Starts the player off with a `Dio's Best Friend`. Functionally equivalent to putting a `Dio's Best Friend` in your `starting_inventory`. | true/false |
|
||||
| item_pickup_step | The number of item pickups which you are allowed to claim before they become an Archipelago location check. | 0 - 5 |
|
||||
| enable_lunar | Allows for lunar items to be shuffled into the item pool on behalf of the Risk of Rain player. | true/false |
|
||||
|
||||
Using the example YAML above: the Risk of Rain 2 player will have 15 total items which they can pick up for other players. (total_locations = 15)
|
||||
|
||||
They will have 15 items waiting for them in the item pool which will be distributed out to the multiworld. (total_locations = 15)
|
||||
|
||||
They will complete a location check every second item. (item_pickup_step = 1)
|
||||
|
||||
They will have 4 of the items which other players can grant them replaced with `Dio's Best Friend`. (total_revivals = 4)
|
||||
|
||||
The player will also start with a `Dio's Best Friend`. (start_with_revive = true)
|
||||
|
||||
The player will have lunar items shuffled into the item pool on their behalf. (enable_lunar = true)
|
||||
@@ -139,5 +139,24 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"gameTitle": "Risk of Rain 2",
|
||||
"tutorials": [
|
||||
{
|
||||
"name": "Multiworld Setup Guide",
|
||||
"description": "A guide to setting up the Risk of Rain 2 integration for Archipelago multiworld games.",
|
||||
"files": [
|
||||
{
|
||||
"language": "English",
|
||||
"filename": "ror2/setup_en.md",
|
||||
"link": "ror2/setup/en",
|
||||
"authors": [
|
||||
"Ijwu"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -53,22 +53,6 @@ can all have different options.
|
||||
The [Generate Game](/player-settings) page on the website allows you to configure your personal settings and
|
||||
export a YAML file from them.
|
||||
|
||||
### Advanced YAML configuration
|
||||
A more advanced version of the YAML file can be created using the [Weighted Settings](/weighted-settings) page,
|
||||
which allows you to configure up to three presets. The Weighted Settings page has many options which are
|
||||
primarily represented with sliders. This allows you to choose how likely certain options are to occur relative
|
||||
to other options within a category.
|
||||
|
||||
For example, imagine the generator creates a bucket labeled "Map Shuffle", and places folded pieces of paper
|
||||
into the bucket for each sub-option. Also imagine your chosen value for "On" is 20, and your value for "Off" is 40.
|
||||
|
||||
In this example, sixty pieces of paper are put into the bucket. Twenty for "On" and forty for "Off". When the
|
||||
generator is deciding whether or not to turn on map shuffle for your game, it reaches into this bucket and pulls
|
||||
out a piece of paper at random. In this example, you are much more likely to have map shuffle turned off.
|
||||
|
||||
If you never want an option to be chosen, simply set its value to zero. Remember that each setting must have at
|
||||
lease one option set to a number greater than zero.
|
||||
|
||||
### Verifying your YAML file
|
||||
If you would like to validate your YAML file to make sure it works, you may do so on the
|
||||
[YAML Validator](/mysterycheck) page.
|
||||
|
||||
@@ -1,486 +0,0 @@
|
||||
let spriteData = null;
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const gameSettings = document.getElementById('weighted-settings');
|
||||
Promise.all([fetchWeightedSettingsYaml(), fetchWeightedSettingsJson(), fetchSpriteData()]).then((results) => {
|
||||
// Load YAML into object
|
||||
const sourceData = jsyaml.safeLoad(results[0], { json: true });
|
||||
const wsVersion = sourceData.ws_version;
|
||||
delete sourceData.ws_version; // Do not include the settings version number in the export
|
||||
|
||||
// Check if settings exist in localStorage. If no settings are present, this is a first load (or reset to default)
|
||||
// and the version number should be silently updated
|
||||
if (!localStorage.getItem('weightedSettings1')) {
|
||||
localStorage.setItem('wsVersion', wsVersion);
|
||||
}
|
||||
|
||||
// Update localStorage with three settings objects. Preserve original objects if present.
|
||||
for (let i=1; i<=3; i++) {
|
||||
const localSettings = JSON.parse(localStorage.getItem(`weightedSettings${i}`));
|
||||
const updatedObj = localSettings ? Object.assign(sourceData, localSettings) : sourceData;
|
||||
localStorage.setItem(`weightedSettings${i}`, JSON.stringify(updatedObj));
|
||||
}
|
||||
|
||||
// Build the entire UI
|
||||
buildUI(JSON.parse(results[1]), JSON.parse(results[2]));
|
||||
|
||||
// Populate the UI and add event listeners
|
||||
populateSettings();
|
||||
document.getElementById('preset-number').addEventListener('change', populateSettings);
|
||||
gameSettings.addEventListener('change', handleOptionChange);
|
||||
gameSettings.addEventListener('keyup', handleOptionChange);
|
||||
|
||||
document.getElementById('export-button').addEventListener('click', exportSettings);
|
||||
document.getElementById('reset-to-default').addEventListener('click', resetToDefaults);
|
||||
adjustHeaderWidth();
|
||||
|
||||
if (localStorage.getItem('wsVersion') !== wsVersion) {
|
||||
const userWarning = document.getElementById('user-warning');
|
||||
const messageSpan = document.createElement('span');
|
||||
messageSpan.innerHTML = "A new version of the weighted settings file is available. Click here to update!" +
|
||||
"<br />Be aware this will also reset your presets, so you should export them now if you want to save them.";
|
||||
userWarning.appendChild(messageSpan);
|
||||
userWarning.style.display = 'block';
|
||||
userWarning.addEventListener('click', resetToDefaults);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
gameSettings.innerHTML = `
|
||||
<h2>Something went wrong while loading your game settings page.</h2>
|
||||
<h2>${error}</h2>
|
||||
<h2><a href="${window.location.origin}">Click here to return to safety!</a></h2>
|
||||
`
|
||||
});
|
||||
document.getElementById('generate-game').addEventListener('click', () => generateGame());
|
||||
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
|
||||
});
|
||||
|
||||
const fetchWeightedSettingsYaml = () => new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status !== 200) {
|
||||
reject("Unable to fetch source yaml file.");
|
||||
return;
|
||||
}
|
||||
resolve(ajax.responseText);
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/static/weightedSettings.yaml` ,true);
|
||||
ajax.send();
|
||||
});
|
||||
|
||||
const fetchWeightedSettingsJson = () => new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status !== 200) {
|
||||
reject('Unable to fetch JSON schema file');
|
||||
return;
|
||||
}
|
||||
resolve(ajax.responseText);
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/static/weightedSettings.json`, true);
|
||||
ajax.send();
|
||||
});
|
||||
|
||||
const fetchSpriteData = () => new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status !== 200) {
|
||||
reject('Unable to fetch sprite data.');
|
||||
return;
|
||||
}
|
||||
resolve(ajax.responseText);
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/generated/spriteData.json`, true);
|
||||
ajax.send();
|
||||
});
|
||||
|
||||
const handleOptionChange = (event) => {
|
||||
if(!event.target.matches('.setting')) { return; }
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`))
|
||||
const settingString = event.target.getAttribute('data-setting');
|
||||
document.getElementById(settingString).innerText = event.target.value;
|
||||
if(getSettingValue(settings, settingString) !== false){
|
||||
const keys = settingString.split('.');
|
||||
switch (keys.length) {
|
||||
case 1:
|
||||
settings[keys[0]] = isNaN(event.target.value) ?
|
||||
event.target.value : parseInt(event.target.value, 10);
|
||||
break;
|
||||
case 2:
|
||||
settings[keys[0]][keys[1]] = isNaN(event.target.value) ?
|
||||
event.target.value : parseInt(event.target.value, 10);
|
||||
break;
|
||||
case 3:
|
||||
settings[keys[0]][keys[1]][keys[2]] = isNaN(event.target.value) ?
|
||||
event.target.value : parseInt(event.target.value, 10);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown setting string received: ${settingString}`)
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the updated settings object bask to localStorage
|
||||
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(settings));
|
||||
}else{
|
||||
console.warn(`Unknown setting string received: ${settingString}`)
|
||||
}
|
||||
};
|
||||
|
||||
const populateSettings = () => {
|
||||
buildSpriteOptions();
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`))
|
||||
const settingsInputs = Array.from(document.querySelectorAll('.setting'));
|
||||
settingsInputs.forEach((input) => {
|
||||
const settingString = input.getAttribute('data-setting');
|
||||
const settingValue = getSettingValue(settings, settingString);
|
||||
if(settingValue !== false){
|
||||
input.value = settingValue;
|
||||
document.getElementById(settingString).innerText = settingValue;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the value of the settings object, or false if the settings object does not exist
|
||||
* @param settings
|
||||
* @param keyString
|
||||
* @returns {string} | bool
|
||||
*/
|
||||
const getSettingValue = (settings, keyString) => {
|
||||
const keys = keyString.split('.');
|
||||
let currentVal = settings;
|
||||
keys.forEach((key) => {
|
||||
if(typeof(key) === 'string' && currentVal.hasOwnProperty(key)){
|
||||
currentVal = currentVal[key];
|
||||
}else{
|
||||
currentVal = false;
|
||||
}
|
||||
});
|
||||
return currentVal;
|
||||
};
|
||||
|
||||
const exportSettings = () => {
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
|
||||
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
||||
download(`${settings.description}.yaml`, yamlText);
|
||||
};
|
||||
|
||||
const resetToDefaults = () => {
|
||||
[1, 2, 3].forEach((presetNumber) => localStorage.removeItem(`weightedSettings${presetNumber}`));
|
||||
location.reload();
|
||||
};
|
||||
|
||||
/** Create an anchor and trigger a download of a text file. */
|
||||
const download = (filename, text) => {
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
|
||||
downloadLink.setAttribute('download', filename);
|
||||
downloadLink.style.display = 'none';
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
};
|
||||
|
||||
const buildUI = (settings, spriteData) => {
|
||||
const settingsWrapper = document.getElementById('settings-wrapper');
|
||||
const settingTypes = {
|
||||
gameOptions: 'Game Options',
|
||||
romOptions: 'ROM Options',
|
||||
}
|
||||
|
||||
Object.keys(settingTypes).forEach((settingTypeKey) => {
|
||||
const sectionHeader = document.createElement('h2');
|
||||
sectionHeader.innerText = settingTypes[settingTypeKey];
|
||||
settingsWrapper.appendChild(sectionHeader);
|
||||
|
||||
Object.values(settings[settingTypeKey]).forEach((setting) => {
|
||||
if (typeof(setting.inputType) === 'undefined' || !setting.inputType){
|
||||
console.error(setting);
|
||||
throw new Error('Setting with no inputType specified.');
|
||||
}
|
||||
|
||||
switch(setting.inputType){
|
||||
case 'text':
|
||||
// Currently, all text input is handled manually because there is very little of it
|
||||
return;
|
||||
case 'range':
|
||||
buildRangeSettings(settingsWrapper, setting);
|
||||
return;
|
||||
default:
|
||||
console.error(setting);
|
||||
throw new Error('Unhandled inputType specified.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Build sprite options
|
||||
const spriteOptionsHeader = document.createElement('h2');
|
||||
spriteOptionsHeader.innerText = 'Sprite Options';
|
||||
settingsWrapper.appendChild(spriteOptionsHeader);
|
||||
|
||||
const spriteOptionsWrapper = document.createElement('div');
|
||||
spriteOptionsWrapper.setAttribute('id', 'sprite-options-wrapper');
|
||||
spriteOptionsWrapper.className = 'setting-wrapper';
|
||||
settingsWrapper.appendChild(spriteOptionsWrapper);
|
||||
|
||||
// Append sprite picker
|
||||
settingsWrapper.appendChild(buildSpritePicker(spriteData));
|
||||
};
|
||||
|
||||
const buildSpriteOptions = () => {
|
||||
const spriteOptionsWrapper = document.getElementById('sprite-options-wrapper');
|
||||
|
||||
// Clear the contents of the wrapper div
|
||||
while(spriteOptionsWrapper.firstChild){
|
||||
spriteOptionsWrapper.removeChild(spriteOptionsWrapper.lastChild);
|
||||
}
|
||||
|
||||
const spriteOptionsTitle = document.createElement('span');
|
||||
spriteOptionsTitle.className = 'title-span';
|
||||
spriteOptionsTitle.innerText = 'Alternate Sprites';
|
||||
spriteOptionsWrapper.appendChild(spriteOptionsTitle);
|
||||
|
||||
const spriteOptionsDescription = document.createElement('span');
|
||||
spriteOptionsDescription.className = 'description-span';
|
||||
spriteOptionsDescription.innerHTML = 'Choose an alternate sprite to play the game with. Additional randomization ' +
|
||||
'options are documented in the ' +
|
||||
'<a href="https://github.com/Berserker66/MultiWorld-Utilities/blob/main/playerSettings.yaml#L374">settings file</a>.';
|
||||
spriteOptionsWrapper.appendChild(spriteOptionsDescription);
|
||||
|
||||
const spriteOptionsTable = document.createElement('table');
|
||||
spriteOptionsTable.setAttribute('id', 'sprite-options-table');
|
||||
spriteOptionsTable.className = 'option-set';
|
||||
const tbody = document.createElement('tbody');
|
||||
tbody.setAttribute('id', 'sprites-tbody');
|
||||
|
||||
const currentPreset = document.getElementById('preset-number').value;
|
||||
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${currentPreset}`));
|
||||
|
||||
// Manually add a row for random sprites
|
||||
addSpriteRow(tbody, playerSettings, 'random');
|
||||
|
||||
// Add a row for each sprite currently present in the player's settings
|
||||
Object.keys(playerSettings.rom.sprite).forEach((spriteName) => {
|
||||
if(['random'].indexOf(spriteName) > -1) return;
|
||||
addSpriteRow(tbody, playerSettings, spriteName)
|
||||
});
|
||||
|
||||
spriteOptionsTable.appendChild(tbody);
|
||||
spriteOptionsWrapper.appendChild(spriteOptionsTable);
|
||||
};
|
||||
|
||||
const buildRangeSettings = (parentElement, settings) => {
|
||||
// Ensure we are operating on a range-specific setting
|
||||
if(typeof(settings.inputType) === 'undefined' || settings.inputType !== 'range'){
|
||||
throw new Error('Invalid input type provided to buildRangeSettings func.');
|
||||
}
|
||||
|
||||
const settingWrapper = document.createElement('div');
|
||||
settingWrapper.className = 'setting-wrapper';
|
||||
|
||||
if(typeof(settings.friendlyName) !== 'undefined' && settings.friendlyName){
|
||||
const sectionTitle = document.createElement('span');
|
||||
sectionTitle.className = 'title-span';
|
||||
sectionTitle.innerText = settings.friendlyName;
|
||||
settingWrapper.appendChild(sectionTitle);
|
||||
}
|
||||
|
||||
if(settings.description){
|
||||
const description = document.createElement('span');
|
||||
description.className = 'description-span';
|
||||
description.innerText = settings.description;
|
||||
settingWrapper.appendChild(description);
|
||||
}
|
||||
|
||||
// Create table
|
||||
const optionSetTable = document.createElement('table');
|
||||
optionSetTable.className = 'option-set';
|
||||
|
||||
// Create table body
|
||||
const tbody = document.createElement('tbody');
|
||||
Object.keys(settings.subOptions).forEach((setting) => {
|
||||
// Overwrite setting key name with real object
|
||||
setting = settings.subOptions[setting];
|
||||
const settingId = (Math.random() * 1000000).toString();
|
||||
|
||||
// Create rows for each option
|
||||
const optionRow = document.createElement('tr');
|
||||
|
||||
// Option name td
|
||||
const optionName = document.createElement('td');
|
||||
optionName.className = 'option-name';
|
||||
const optionLabel = document.createElement('label');
|
||||
optionLabel.setAttribute('for', settingId);
|
||||
optionLabel.setAttribute('data-tooltip', setting.description);
|
||||
optionLabel.innerText = setting.friendlyName;
|
||||
optionName.appendChild(optionLabel);
|
||||
optionRow.appendChild(optionName);
|
||||
|
||||
// Option value td
|
||||
const optionValue = document.createElement('td');
|
||||
optionValue.className = 'option-value';
|
||||
const input = document.createElement('input');
|
||||
input.className = 'setting';
|
||||
input.setAttribute('id', settingId);
|
||||
input.setAttribute('type', 'range');
|
||||
input.setAttribute('min', '0');
|
||||
input.setAttribute('max', '100');
|
||||
input.setAttribute('data-setting', setting.keyString);
|
||||
input.value = setting.defaultValue;
|
||||
optionValue.appendChild(input);
|
||||
const valueDisplay = document.createElement('span');
|
||||
valueDisplay.setAttribute('id', setting.keyString);
|
||||
valueDisplay.innerText = setting.defaultValue;
|
||||
optionValue.appendChild(valueDisplay);
|
||||
optionRow.appendChild(optionValue);
|
||||
tbody.appendChild(optionRow);
|
||||
});
|
||||
|
||||
optionSetTable.appendChild(tbody);
|
||||
settingWrapper.appendChild(optionSetTable);
|
||||
parentElement.appendChild(settingWrapper);
|
||||
};
|
||||
|
||||
const addSpriteRow = (tbody, playerSettings, spriteName) => {
|
||||
const rowId = (Math.random() * 1000000).toString();
|
||||
const optionId = (Math.random() * 1000000).toString();
|
||||
const tr = document.createElement('tr');
|
||||
tr.setAttribute('id', rowId);
|
||||
|
||||
// Option Name
|
||||
const optionName = document.createElement('td');
|
||||
optionName.className = 'option-name';
|
||||
const label = document.createElement('label');
|
||||
label.htmlFor = optionId;
|
||||
label.innerText = spriteName;
|
||||
optionName.appendChild(label);
|
||||
|
||||
if(['random', 'random_sprite_on_event'].indexOf(spriteName) === -1) {
|
||||
const deleteButton = document.createElement('span');
|
||||
deleteButton.setAttribute('data-sprite', spriteName);
|
||||
deleteButton.setAttribute('data-row-id', rowId);
|
||||
deleteButton.innerText = ' (❌)';
|
||||
deleteButton.className = 'delete-button';
|
||||
optionName.appendChild(deleteButton);
|
||||
deleteButton.addEventListener('click', removeSpriteOption);
|
||||
}
|
||||
|
||||
tr.appendChild(optionName);
|
||||
|
||||
// Option Value
|
||||
const optionValue = document.createElement('td');
|
||||
optionValue.className = 'option-value';
|
||||
const input = document.createElement('input');
|
||||
input.className = 'setting';
|
||||
input.setAttribute('id', optionId);
|
||||
input.setAttribute('type', 'range');
|
||||
input.setAttribute('min', '0');
|
||||
input.setAttribute('max', '100');
|
||||
input.setAttribute('data-setting', `rom.sprite.${spriteName}`);
|
||||
input.value = "50";
|
||||
optionValue.appendChild(input);
|
||||
|
||||
// Value display
|
||||
const valueDisplay = document.createElement('span');
|
||||
valueDisplay.setAttribute('id', `rom.sprite.${spriteName}`);
|
||||
valueDisplay.innerText = playerSettings.rom.sprite.hasOwnProperty(spriteName) ?
|
||||
playerSettings.rom.sprite[spriteName] : '0';
|
||||
optionValue.appendChild(valueDisplay);
|
||||
|
||||
tr.appendChild(optionValue);
|
||||
tbody.appendChild(tr);
|
||||
};
|
||||
|
||||
const addSpriteOption = (event) => {
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
|
||||
const spriteName = event.target.getAttribute('data-sprite');
|
||||
|
||||
if (Object.keys(playerSettings.rom.sprite).indexOf(spriteName) !== -1) {
|
||||
// Do not add the same sprite twice
|
||||
return;
|
||||
}
|
||||
|
||||
// Add option to playerSettings object
|
||||
playerSettings.rom.sprite[event.target.getAttribute('data-sprite')] = 50;
|
||||
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(playerSettings));
|
||||
|
||||
// Add <tr> to #sprite-options-table
|
||||
const tbody = document.getElementById('sprites-tbody');
|
||||
addSpriteRow(tbody, playerSettings, spriteName);
|
||||
};
|
||||
|
||||
const removeSpriteOption = (event) => {
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
|
||||
const spriteName = event.target.getAttribute('data-sprite');
|
||||
|
||||
// Remove option from playerSettings object
|
||||
delete playerSettings.rom.sprite[spriteName];
|
||||
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(playerSettings));
|
||||
|
||||
// Remove <tr> from #sprite-options-table
|
||||
const tr = document.getElementById(event.target.getAttribute('data-row-id'));
|
||||
tr.parentNode.removeChild(tr);
|
||||
};
|
||||
|
||||
const buildSpritePicker = (spriteData) => {
|
||||
const spritePicker = document.createElement('div');
|
||||
spritePicker.setAttribute('id', 'sprite-picker');
|
||||
|
||||
// Build description
|
||||
const description = document.createElement('span');
|
||||
description.innerText = 'To add a sprite to your playable list, click the one you want below.';
|
||||
spritePicker.appendChild(description);
|
||||
|
||||
const sprites = document.createElement('div');
|
||||
sprites.setAttribute('id', 'sprite-picker-sprites');
|
||||
spriteData.sprites.forEach((sprite) => {
|
||||
const spriteImg = document.createElement('img');
|
||||
let spriteGifFile = sprite.file.split('.');
|
||||
spriteGifFile.pop();
|
||||
spriteGifFile = spriteGifFile.join('.') + '.gif';
|
||||
spriteImg.setAttribute('src', `static/generated/sprites/${spriteGifFile}`);
|
||||
spriteImg.setAttribute('data-sprite', sprite.file.split('.')[0]);
|
||||
spriteImg.setAttribute('alt', sprite.name);
|
||||
|
||||
// Wrap the image in a span to allow for tooltip presence
|
||||
const imgWrapper = document.createElement('span');
|
||||
imgWrapper.className = 'sprite-img-wrapper';
|
||||
imgWrapper.setAttribute('data-tooltip', `${sprite.name}${sprite.author ? `, by ${sprite.author}` : ''}`);
|
||||
imgWrapper.appendChild(spriteImg);
|
||||
imgWrapper.setAttribute('data-sprite', sprite.name);
|
||||
sprites.appendChild(imgWrapper);
|
||||
imgWrapper.addEventListener('click', addSpriteOption);
|
||||
});
|
||||
|
||||
spritePicker.appendChild(sprites);
|
||||
return spritePicker;
|
||||
};
|
||||
|
||||
const generateGame = (raceMode = false) => {
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
axios.post('/api/generate', {
|
||||
weights: { player: localStorage.getItem(`weightedSettings${presetNumber}`) },
|
||||
presetData: { player: localStorage.getItem(`weightedSettings${presetNumber}`) },
|
||||
playerCount: 1,
|
||||
race: raceMode ? '1' : '0',
|
||||
}).then((response) => {
|
||||
window.location.href = response.data.url;
|
||||
}).catch((error) => {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.innerText = 'Something went wrong and your game could not be generated.';
|
||||
if (error.response.data.text) {
|
||||
userMessage.innerText += ' ' + error.response.data.text;
|
||||
}
|
||||
userMessage.classList.add('visible');
|
||||
window.scrollTo(0, 0);
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
@@ -60,7 +60,7 @@ html{
|
||||
width: 200px;
|
||||
height: calc(156px - 40px);
|
||||
padding-top: 40px;
|
||||
cursor: default;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#mid-left-button{
|
||||
|
||||
120
WebHostLib/static/styles/markdown.css
Normal file
120
WebHostLib/static/styles/markdown.css
Normal file
@@ -0,0 +1,120 @@
|
||||
.markdown{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 70rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1rem 3rem;
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
.markdown img{
|
||||
max-width: 100%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.markdown p{
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.markdown a{
|
||||
color: #ffef00;
|
||||
}
|
||||
|
||||
.markdown h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
.markdown h2{
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffe993;
|
||||
text-transform: lowercase;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
.markdown h3{
|
||||
font-size: 1.70rem;
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown h4{
|
||||
font-size: 1.5rem;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown h5{
|
||||
font-size: 1.25rem;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.markdown h6{
|
||||
font-size: 1.25rem;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
color: #434343;
|
||||
}
|
||||
|
||||
.markdown h3, .markdown h4, .markdown h5,.markdown h6{
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.markdown ul{
|
||||
|
||||
}
|
||||
|
||||
.markdown ol{
|
||||
|
||||
}
|
||||
|
||||
.markdown li{
|
||||
|
||||
}
|
||||
|
||||
.markdown pre{
|
||||
margin-top: 0;
|
||||
padding: 0.5rem 0.25rem;
|
||||
background-color: #ffeeab;
|
||||
border: 1px solid #9f916a;
|
||||
border-radius: 6px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.markdown code{
|
||||
background-color: #ffeeab;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.markdown #tutorial-video-container{
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.markdown #language-selector-wrapper{
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
#weighted-settings{
|
||||
width: 60rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
#user-warning, #weighted-settings #user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings #user-message.visible{
|
||||
display: block;
|
||||
}
|
||||
|
||||
#weighted-settings code{
|
||||
background-color: #d9cd8e;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#weighted-settings h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
#weighted-settings h2{
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffe993;
|
||||
text-transform: lowercase;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
#weighted-settings h3, #weighted-settings h4, #weighted-settings h5, #weighted-settings h6{
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#weighted-settings .instructions{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper .setting-wrapper{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper .setting-wrapper .title-span{
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper{
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper #sprite-picker{
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper #sprite-picker #sprite-picker-sprites{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper #sprite-picker .sprite-img-wrapper{
|
||||
cursor: pointer;
|
||||
margin: 10px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
/* Center tooltip text for sprite images */
|
||||
#weighted-settings #settings-wrapper #sprite-picker .sprite-img-wrapper::after{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper #sprite-picker .sprite-img-wrapper img{
|
||||
width: 32px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
#weighted-settings table.option-set{
|
||||
width: 100%;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
#weighted-settings table.option-set td.option-name{
|
||||
width: 150px;
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
#weighted-settings table.option-set td.option-name .delete-button{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings table.option-set td.option-value{
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
#weighted-settings table.option-set td.option-value input[type=range]{
|
||||
width: 90%;
|
||||
min-width: 300px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#weighted-settings #weighted-settings-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#weighted-settings a{
|
||||
color: #ffef00;
|
||||
}
|
||||
|
||||
#weighted-settings input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
#weighted-settings input:not([type]):focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
#weighted-settings select{
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
17
WebHostLib/templates/faq.html
Normal file
17
WebHostLib/templates/faq.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<title>Frequently Asked Questions</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
|
||||
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
|
||||
crossorigin="anonymous"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/faq.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div id="faq-wrapper" data-lang="{{ lang }}" class="markdown">
|
||||
<!-- Content generated by JavaScript -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -9,9 +9,9 @@
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="games">
|
||||
<h1>Currently Supported Games</h1>
|
||||
{% for game, (display_name, description) in games_list.items() %}
|
||||
<h3><a href="{{ url_for("game_page", game=game) }}">{{ display_name}}</a></h3>
|
||||
<p>{{ description}}</p>
|
||||
{% for game, description in worlds.items() %}
|
||||
<h3><a href="{{ url_for("game_page", game=game) }}/player-settings">{{ game }}</a></h3>
|
||||
<p>{{ description }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
<p>
|
||||
This page allows you to generate a game by uploading a yaml file or a zip file containing yaml files.
|
||||
If you do not have a config (yaml) file yet, you may create one on the
|
||||
<a href="/player-settings">Player Settings</a> page. If you would like more advanced options,
|
||||
the <a href="/weighted-settings">Weighted Settings</a> page might be what you're looking for.
|
||||
<a href="/player-settings">Player Settings</a> page.
|
||||
</p>
|
||||
<p>
|
||||
{% if race -%}
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
<a href="/">archipelago</a>
|
||||
</div>
|
||||
<div id="base-header-right">
|
||||
<a href="/games">games</a>
|
||||
<a href="/games">supported games</a>
|
||||
<a href="/tutorial">setup guides</a>
|
||||
<a href="https://discord.gg/8Z65BR2">discord</a>
|
||||
<a href="/uploads">start game</a>
|
||||
<a href="/faq/en">f.a.q.</a>
|
||||
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
|
||||
</div>
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
<div id="landing-wrapper">
|
||||
<div id="landing-header">
|
||||
<h1>ARCHIPELAGO</h1>
|
||||
<h4>multiworld randomizer ecosystem</h4>
|
||||
<h4>multiworld multi-game randomizer</h4>
|
||||
</div>
|
||||
<div id="landing-links">
|
||||
<a href="/games" id="mid-button">start<br />playing</a>
|
||||
<a id="far-left-button"></a>
|
||||
<a href="/tutorial" id="mid-left-button">setup guide</a>
|
||||
<a href="/uploads" id="far-right-button">Host Game</a>
|
||||
<a href="https://discord.gg/8Z65BR2" id="mid-right-button">discord</a>
|
||||
<a href="/uploads" id="mid-button">start<br />game</a>
|
||||
<a href="/games" id="far-left-button">supported<br />games</a>
|
||||
<a href="/tutorial" id="mid-left-button">setup guides</a>
|
||||
<a href="https://discord.gg/8Z65BR2" id="far-right-button" target="_blank">discord</a>
|
||||
<a href="/faq/en/" id="mid-right-button">f.a.q.</a>
|
||||
</div>
|
||||
<div id="landing-clouds">
|
||||
<img id="cloud1" src="/static/static/backgrounds/clouds/cloud-0001.png"/>
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
{% elif patch.game == "Factorio" %}
|
||||
<li><a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}">
|
||||
Mod for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
||||
{% elif patch.game == "Ocarina of Time" %}
|
||||
<li><a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}">
|
||||
APZ5 for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
||||
{% else %}
|
||||
<li><a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}">
|
||||
Patch for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
# http://www.yamllint.com/
|
||||
|
||||
description: Default {{ game }} Template # Used to describe your yaml. Useful if you have multiple files
|
||||
name: YourName{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
|
||||
# Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
|
||||
name: YourName{number}
|
||||
#{player} will be replaced with the player's slot number.
|
||||
#{PLAYER} will be replaced with the player's slot number if that slot number is greater than 1.
|
||||
#{number} will be replaced with the counter value of the name.
|
||||
@@ -51,21 +52,24 @@ progression_balancing:
|
||||
# - "Progressive Weapons"
|
||||
# exclude_locations: # Force certain locations to never contain progression items, and always be filled with junk.
|
||||
# - "Master Sword Pedestal"
|
||||
{{ game }}:
|
||||
{%- for option_name, option in options.items() %}
|
||||
{{ option_name }}:{% if option.__doc__ %} # {{ option.__doc__ }}{% endif %}
|
||||
{%- if option.range_start is defined %}
|
||||
{%- macro range_option(option) %}
|
||||
# you can add additional values between minimum and maximum
|
||||
{{ option.range_start }}: 0 # minimum value
|
||||
{{ option.range_end }}: 0 # maximum value
|
||||
random: 50
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
{%- set data, notes = dictify_range(option) %}
|
||||
{%- for entry, default in data.items() %}
|
||||
{{ entry }}: {{ default }}{% if notes[entry] %} # {{ notes[entry] }}{% endif %}
|
||||
{%- endfor -%}
|
||||
{% endmacro %}
|
||||
{{ game }}:
|
||||
{%- for option_key, option in options.items() %}
|
||||
{{ option_key }}:{% if option.__doc__ %} # {{ option.__doc__ | replace('\n', '\n#') | indent(4, first=False) }}{% endif %}
|
||||
{%- if option.range_start is defined %}
|
||||
{{- range_option(option) -}}
|
||||
{%- elif option.options -%}
|
||||
{%- for sub_option_name, suboption_option_id in option.options.items() %}
|
||||
{%- for suboption_option_id, sub_option_name in option.name_lookup.items() %}
|
||||
{{ sub_option_name }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %}
|
||||
{%- endfor -%}
|
||||
{%- else %}
|
||||
{{ yaml_dump(option.default) | indent(4, first=False) }}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endfor %}
|
||||
{% if not options %}{}{% endif %}
|
||||
|
||||
@@ -14,11 +14,14 @@
|
||||
<div id="user-message"></div>
|
||||
<h1><span id="game-name">Player</span> Settings</h1>
|
||||
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
|
||||
or download a settings file you can use to participate in a MultiWorld. If you would like to make
|
||||
your settings extra random, check out the advanced <a href="/weighted-settings">weighted settings</a>
|
||||
page.</p>
|
||||
or download a settings file you can use to participate in a MultiWorld.</p>
|
||||
|
||||
<p>A list of all games you have generated can be found <a href="/user-content">here</a>.</p>
|
||||
<p>
|
||||
A list of all games you have generated can be found <a href="/user-content">here</a>.
|
||||
<br />
|
||||
Advanced users can download a template file for this game
|
||||
<a href="/static/generated/{{ game }}.yaml">here</a>.
|
||||
</p>
|
||||
|
||||
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
||||
items if you are playing in a MultiWorld.</label><br />
|
||||
|
||||
21
WebHostLib/templates/templates.html
Normal file
21
WebHostLib/templates/templates.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<title>Option Templates (YAML)</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
|
||||
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
|
||||
crossorigin="anonymous"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="markdown">
|
||||
<h1>Option Templates (YAML)</h1>
|
||||
<ul>
|
||||
{% for file in files %}
|
||||
<li><a href="{{ url_for('static', filename="generated/"+file+".yaml") }}">{{ file }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,77 +0,0 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Player Settings</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weightedSettings.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weightedSettings.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="weighted-settings">
|
||||
<header id="user-warning"></header>
|
||||
<div id="user-message"></div>
|
||||
<h1>Weighted Settings</h1>
|
||||
<div id="instructions">
|
||||
This page is used to configure your weighted settings. You have three presets you can control, which
|
||||
you can access using the dropdown menu below. These settings will be usable when generating a
|
||||
single player game, or you can export them to a <code>.yaml</code> file and use them in a multiworld.
|
||||
If you already have a settings file you would like to validate, you may do so on the
|
||||
<a href="/mysterycheck">verification page</a>.
|
||||
</div>
|
||||
|
||||
<div id="settings-wrapper">
|
||||
<div class="setting-wrapper">
|
||||
Choose a preset and optionally assign it a nickname, which will be used as the file's description if
|
||||
you download it.
|
||||
<table class="option-set">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="option-name">
|
||||
<label for="preset-number">Preset Number:</label>
|
||||
</td>
|
||||
<td class="option-value">
|
||||
<select id="preset-number">
|
||||
<option value="1">Preset 1</option>
|
||||
<option value="2">Preset 2</option>
|
||||
<option value="3">Preset 3</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="option-name">
|
||||
<label for="description">Preset Name:</label>
|
||||
</td>
|
||||
<td class="option-value">
|
||||
<input id="description" class="setting" data-setting="description" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Choose a name you want to represent you in-game. This will appear when you send items
|
||||
to other people in multiworld games.
|
||||
<table class="option-set">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="option-name">
|
||||
<label for="name">Player Name:</label>
|
||||
</td>
|
||||
<td class="option-value">
|
||||
<input id="name" maxlength="16" class="setting" data-setting="name" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="weighted-settings-button-row">
|
||||
<button id="reset-to-default">Reset to Defaults</button>
|
||||
<button id="export-button">Export Settings</button>
|
||||
<button id="generate-game">Generate Game</button>
|
||||
<button id="generate-race">Generate Race</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -58,11 +58,17 @@ def uploads():
|
||||
game="Minecraft"))
|
||||
|
||||
elif file.filename.endswith(".zip"):
|
||||
# Factorio mods needs a specific name or they do no function
|
||||
# Factorio mods needs a specific name or they do not function
|
||||
_, seed_name, slot_id, slot_name = file.filename.rsplit("_", 1)[0].split("-")
|
||||
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
|
||||
player_id=int(slot_id[1:]), game="Factorio"))
|
||||
|
||||
elif file.filename.endswith(".apz5"):
|
||||
# .apz5 must be named specifically since they don't contain any metadata
|
||||
_, seed_name, slot_id, slot_name = file.filename.split('.')[0].split('_', 3)
|
||||
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
|
||||
player_id=int(slot_id[1:]), game="Ocarina of Time"))
|
||||
|
||||
elif file.filename.endswith(".txt"):
|
||||
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")
|
||||
elif file.filename.endswith(".archipelago"):
|
||||
|
||||
@@ -85,4 +85,7 @@ factorio_options:
|
||||
executable: "factorio\\bin\\x64\\factorio"
|
||||
minecraft_options:
|
||||
forge_directory: "Minecraft Forge server"
|
||||
max_heap_size: "2G"
|
||||
max_heap_size: "2G"
|
||||
oot_options:
|
||||
# File name of the OoT v1.0 ROM
|
||||
rom_file: "The Legend of Zelda - Ocarina of Time.z64"
|
||||
@@ -61,7 +61,7 @@ NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-mod
|
||||
|
||||
[Files]
|
||||
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/lttp or generator
|
||||
Source: "{#sourcepath}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, *exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{#sourcepath}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/lttp
|
||||
Source: "{#sourcepath}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator
|
||||
|
||||
@@ -243,7 +243,8 @@ begin
|
||||
end;
|
||||
finally
|
||||
if( isJavaNeeded() ) then
|
||||
UnZip(ExpandConstant('{tmp}')+'\java.zip',ExpandConstant('{app}'));
|
||||
if(ForceDirectories(ExpandConstant('{app}'))) then
|
||||
UnZip(ExpandConstant('{tmp}')+'\java.zip',ExpandConstant('{app}'));
|
||||
MinecraftDownloadPage.Hide;
|
||||
end;
|
||||
Result := True;
|
||||
|
||||
@@ -28,6 +28,7 @@ game: # Pick a game to play
|
||||
Factorio: 0
|
||||
Minecraft: 0
|
||||
Subnautica: 0
|
||||
Slay the Spire: 0
|
||||
requires:
|
||||
version: 0.1.6 # Version of Archipelago required for this yaml to work as expected.
|
||||
# Shared Options supported by all games:
|
||||
@@ -56,7 +57,23 @@ progression_balancing:
|
||||
# - "Master Sword Pedestal"
|
||||
|
||||
Subnautica: {}
|
||||
|
||||
Slay the Spire:
|
||||
character: # Pick What Character you wish to play with.
|
||||
ironclad: 50
|
||||
silent: 50
|
||||
defect: 50
|
||||
watcher: 50
|
||||
ascension: # What Ascension do you wish to play with.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 50 # minimum value
|
||||
20: 0 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
heart_run: # Whether or not you will need to collect they 3 keys to unlock the final act
|
||||
# and beat the heart to finish the game.
|
||||
false: 50
|
||||
true: 0
|
||||
Factorio:
|
||||
tech_tree_layout:
|
||||
single: 1
|
||||
@@ -259,6 +276,9 @@ Minecraft:
|
||||
50: 0
|
||||
75: 0
|
||||
100: 0
|
||||
send_defeated_mobs: # Send killed mobs to other Minecraft worlds which have this option enabled.
|
||||
on: 0
|
||||
off: 1
|
||||
A Link to the Past:
|
||||
### Logic Section ###
|
||||
glitches_required: # Determine the logic required to complete the seed
|
||||
@@ -276,29 +296,31 @@ A Link to the Past:
|
||||
on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items
|
||||
off: 50
|
||||
### End of Logic Section ###
|
||||
map_shuffle: # Shuffle dungeon maps into the world and other dungeons, including other players' worlds
|
||||
on: 0
|
||||
off: 50
|
||||
compass_shuffle: # Shuffle compasses into the world and other dungeons, including other players' worlds
|
||||
on: 0
|
||||
off: 50
|
||||
smallkey_shuffle: # Shuffle small keys into the world and other dungeons, including other players' worlds
|
||||
on: 0
|
||||
universal: 0 # allows small keys to be used in any dungeon and adds shops to buy more
|
||||
off: 50
|
||||
bigkey_shuffle: # Shuffle big keys into the world and other dungeons, including other players' worlds
|
||||
on: 0
|
||||
off: 50
|
||||
local_keys: # Keep small keys and big keys local to your world
|
||||
on: 0
|
||||
off: 50
|
||||
dungeon_items: # Alternative to the 4 shuffles and local_keys above this, does nothing until the respective 4 shuffles and local_keys above are deleted
|
||||
mc: 0 # Shuffle maps and compasses
|
||||
none: 50 # Shuffle none of the 4
|
||||
mcsb: 0 # Shuffle all of the 4, any combination of m, c, s and b will shuffle the respective item, or not if it's missing, so you can add more options here
|
||||
lmcsb: 0 # Like mcsb above, but with keys kept local to your world. l is what makes your keys local, or not if it's missing
|
||||
ub: 0 # universal small keys and shuffled big keys
|
||||
# you can add more combos of these letters here
|
||||
bigkey_shuffle: # Big Key Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
smallkey_shuffle: # Small Key Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
universal: 0
|
||||
compass_shuffle: # Compass Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
map_shuffle: # Map Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
dungeon_counters:
|
||||
on: 0 # Always display amount of items checked in a dungeon
|
||||
pickup: 50 # Show when compass is picked up
|
||||
@@ -652,6 +674,618 @@ A Link to the Past:
|
||||
assured: 0 # Begin with a sword, the rest are placed randomly throughout the world
|
||||
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
|
||||
swordless: 0 # swordless mode
|
||||
|
||||
Ocarina of Time:
|
||||
logic_rules: # Set the logic used for the generator.
|
||||
glitchless: 50
|
||||
glitched: 0
|
||||
no_logic: 0
|
||||
logic_no_night_tokens_without_suns_song: # Nighttime skulltulas will logically require Sun's Song.
|
||||
false: 50
|
||||
true: 0
|
||||
open_forest: # Set the state of Kokiri Forest and the path to Deku Tree.
|
||||
open: 50
|
||||
closed_deku: 0
|
||||
closed: 0
|
||||
open_kakariko: # Set the state of the Kakariko Village gate.
|
||||
open: 50
|
||||
zelda: 0
|
||||
closed: 0
|
||||
open_door_of_time: # Open the Door of Time by default, without the Song of Time.
|
||||
false: 0
|
||||
true: 50
|
||||
zora_fountain: # Set the state of King Zora, blocking the way to Zora's Fountain.
|
||||
open: 0
|
||||
adult: 0
|
||||
closed: 50
|
||||
gerudo_fortress: # Set the requirements for access to Gerudo Fortress.
|
||||
normal: 0
|
||||
fast: 50
|
||||
open: 0
|
||||
bridge: # Set the requirements for the Rainbow Bridge.
|
||||
open: 0
|
||||
vanilla: 0
|
||||
stones: 0
|
||||
medallions: 50
|
||||
dungeons: 0
|
||||
tokens: 0
|
||||
trials: # Set the number of required trials in Ganon's Castle.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
6: 0 # maximum value
|
||||
random: 50
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
starting_age: # Choose which age Link will start as.
|
||||
child: 50
|
||||
adult: 0
|
||||
triforce_hunt: # Gather pieces of the Triforce scattered around the world to complete the game.
|
||||
false: 50
|
||||
true: 0
|
||||
triforce_goal: # Number of Triforce pieces required to complete the game. Total number placed determined by the Item Pool setting.
|
||||
# you can add additional values between minimum and maximum
|
||||
1: 0 # minimum value
|
||||
50: 0 # maximum value
|
||||
random: 50
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
bombchus_in_logic: # Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell refills; bombchus open Bombchu Bowling.
|
||||
false: 50
|
||||
true: 0
|
||||
bridge_stones: # Set the number of Spiritual Stones required for the rainbow bridge.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
3: 0 # maximum value
|
||||
random: 50
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
bridge_medallions: # Set the number of medallions required for the rainbow bridge.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
6: 0 # maximum value
|
||||
random: 50
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
bridge_rewards: # Set the number of dungeon rewards required for the rainbow bridge.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
9: 0 # maximum value
|
||||
random: 50
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
bridge_tokens: # Set the number of Gold Skulltula Tokens required for the rainbow bridge.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
100: 0 # maximum value
|
||||
random: 50
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
shuffle_mapcompass: # Control where to shuffle dungeon maps and compasses.
|
||||
remove: 0
|
||||
startwith: 50
|
||||
vanilla: 0
|
||||
dungeon: 0
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_smallkeys: # Control where to shuffle dungeon small keys.
|
||||
remove: 0
|
||||
vanilla: 0
|
||||
dungeon: 50
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_fortresskeys: # Control where to shuffle the Gerudo Fortress small keys.
|
||||
vanilla: 50
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_bosskeys: # Control where to shuffle boss keys, except the Ganon's Castle Boss Key.
|
||||
remove: 0
|
||||
vanilla: 0
|
||||
dungeon: 50
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_ganon_bosskey: # Control where to shuffle the Ganon's Castle Boss Key.
|
||||
remove: 50
|
||||
vanilla: 0
|
||||
dungeon: 0
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
on_lacs: 0
|
||||
enhance_map_compass: # Map tells if a dungeon is vanilla or MQ. Compass tells what the dungeon reward is.
|
||||
false: 50
|
||||
true: 0
|
||||
lacs_condition: # Set the requirements for the Light Arrow Cutscene in the Temple of Time.
|
||||
vanilla: 50
|
||||
stones: 0
|
||||
medallions: 0
|
||||
dungeons: 0
|
||||
tokens: 0
|
||||
lacs_stones: # Set the number of Spiritual Stones required for LACS.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
3: 0 # maximum value
|
||||
random: 50
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
lacs_medallions: # Set the number of medallions required for LACS.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
6: 0 # maximum value
|
||||
random: 50
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
lacs_rewards: # Set the number of dungeon rewards required for LACS.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
9: 0 # maximum value
|
||||
random: 50
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
lacs_tokens: # Set the number of Gold Skulltula Tokens required for LACS.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
100: 0 # maximum value
|
||||
random: 50
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
shuffle_song_items: # Set where songs can appear.
|
||||
song: 50
|
||||
dungeon: 0
|
||||
any: 0
|
||||
shopsanity: # Randomizes shop contents. Set to "off" to not shuffle shops; "0" shuffles shops but does not allow multiworld items in shops.
|
||||
off: 50
|
||||
"0": 0
|
||||
"1": 0
|
||||
"2": 0
|
||||
"3": 0
|
||||
"4": 0
|
||||
random_value: 0
|
||||
tokensanity: # Token rewards from Gold Skulltulas are shuffled into the pool.
|
||||
off: 50
|
||||
dungeons: 0
|
||||
overworld: 0
|
||||
all: 0
|
||||
shuffle_scrubs: # Shuffle the items sold by Business Scrubs, and set the prices.
|
||||
off: 50
|
||||
low: 0
|
||||
affordable: 0
|
||||
expensive: 0
|
||||
shuffle_cows: # Cows give items when Epona's Song is played.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_kokiri_sword: # Shuffle Kokiri Sword into the item pool.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_ocarinas: # Shuffle the Fairy Ocarina and Ocarina of Time into the item pool.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_weird_egg: # Shuffle the Weird Egg from Malon at Hyrule Castle.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_gerudo_card: # Shuffle the Gerudo Membership Card into the item pool.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_beans: # Adds a pack of 10 beans to the item pool and changes the bean salesman to sell one item for 60 rupees.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_medigoron_carpet_salesman: # Shuffle the items sold by Medigoron and the Haunted Wasteland Carpet Salesman.
|
||||
false: 50
|
||||
true: 0
|
||||
skip_child_zelda: # Game starts with Zelda's Letter, the item at Zelda's Lullaby, and the relevant events already completed.
|
||||
false: 50
|
||||
true: 0
|
||||
no_escape_sequence: # Skips the tower collapse sequence between the Ganondorf and Ganon fights.
|
||||
false: 0
|
||||
true: 50
|
||||
no_guard_stealth: # The crawlspace into Hyrule Castle skips straight to Zelda.
|
||||
false: 0
|
||||
true: 50
|
||||
no_epona_race: # Epona can always be summoned with Epona's Song.
|
||||
false: 0
|
||||
true: 50
|
||||
skip_some_minigame_phases: # Dampe Race and Horseback Archery give both rewards if the second condition is met on the first attempt.
|
||||
false: 0
|
||||
true: 50
|
||||
complete_mask_quest: # All masks are immediately available to borrow from the Happy Mask Shop.
|
||||
false: 50
|
||||
true: 0
|
||||
useful_cutscenes: # Reenables the Poe cutscene in Forest Temple, Darunia in Fire Temple, and Twinrova introduction. Mostly useful for glitched.
|
||||
false: 50
|
||||
true: 0
|
||||
fast_chests: # All chest animations are fast. If disabled, major items have a slow animation.
|
||||
false: 0
|
||||
true: 50
|
||||
free_scarecrow: # Pulling out the ocarina near a scarecrow spot spawns Pierre without needing the song.
|
||||
false: 50
|
||||
true: 0
|
||||
fast_bunny_hood: # Bunny Hood lets you move 1.5x faster like in Majora's Mask.
|
||||
false: 50
|
||||
true: 0
|
||||
chicken_count: # Controls the number of Cuccos for Anju to give an item as child.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
7: 0 # maximum value
|
||||
random: 50
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
hints: # Gossip Stones can give hints about item locations.
|
||||
none: 0
|
||||
mask: 0
|
||||
agony: 0
|
||||
always: 50
|
||||
false: 0
|
||||
hint_dist: # Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.
|
||||
balanced: 50
|
||||
ddr: 0
|
||||
league: 0
|
||||
mw2: 0
|
||||
scrubs: 0
|
||||
strong: 0
|
||||
tournament: 0
|
||||
useless: 0
|
||||
very_strong: 0
|
||||
text_shuffle: # Randomizes text in the game for comedic effect.
|
||||
none: 50
|
||||
except_hints: 0
|
||||
complete: 0
|
||||
damage_multiplier: # Controls the amount of damage Link takes.
|
||||
half: 0
|
||||
normal: 50
|
||||
double: 0
|
||||
quadruple: 0
|
||||
ohko: 0
|
||||
no_collectible_hearts: # Hearts will not drop from enemies or objects.
|
||||
false: 50
|
||||
true: 0
|
||||
starting_tod: # Change the starting time of day.
|
||||
default: 50
|
||||
sunrise: 0
|
||||
morning: 0
|
||||
noon: 0
|
||||
afternoon: 0
|
||||
sunset: 0
|
||||
evening: 0
|
||||
midnight: 0
|
||||
witching_hour: 0
|
||||
start_with_consumables: # Start the game with full Deku Sticks and Deku Nuts.
|
||||
false: 50
|
||||
true: 0
|
||||
start_with_rupees: # Start with a full wallet. Wallet upgrades will also fill your wallet.
|
||||
false: 50
|
||||
true: 0
|
||||
item_pool_value: # Changes the number of items available in the game.
|
||||
plentiful: 0
|
||||
balanced: 50
|
||||
scarce: 0
|
||||
minimal: 0
|
||||
junk_ice_traps: # Adds ice traps to the item pool.
|
||||
off: 0
|
||||
normal: 50
|
||||
extra: 0
|
||||
mayhem: 0
|
||||
onslaught: 0
|
||||
ice_trap_appearance: # Changes the appearance of ice traps as freestanding items.
|
||||
major_only: 50
|
||||
junk_only: 0
|
||||
anything: 0
|
||||
logic_earliest_adult_trade: # Earliest item that can appear in the adult trade sequence.
|
||||
pocket_egg: 0
|
||||
pocket_cucco: 0
|
||||
cojiro: 0
|
||||
odd_mushroom: 0
|
||||
poachers_saw: 0
|
||||
broken_sword: 0
|
||||
prescription: 50
|
||||
eyeball_frog: 0
|
||||
eyedrops: 0
|
||||
claim_check: 0
|
||||
logic_latest_adult_trade: # Latest item that can appear in the adult trade sequence.
|
||||
pocket_egg: 0
|
||||
pocket_cucco: 0
|
||||
cojiro: 0
|
||||
odd_mushroom: 0
|
||||
poachers_saw: 0
|
||||
broken_sword: 0
|
||||
prescription: 0
|
||||
eyeball_frog: 0
|
||||
eyedrops: 0
|
||||
claim_check: 50
|
||||
default_targeting: # Default targeting option.
|
||||
hold: 50
|
||||
switch: 0
|
||||
display_dpad: # Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots).
|
||||
false: 0
|
||||
true: 50
|
||||
correct_model_colors: # Makes in-game models match their HUD element colors.
|
||||
false: 0
|
||||
true: 50
|
||||
background_music: # Randomize or disable background music.
|
||||
normal: 50
|
||||
off: 0
|
||||
randomized: 0
|
||||
fanfares: # Randomize or disable item fanfares.
|
||||
normal: 50
|
||||
off: 0
|
||||
randomized: 0
|
||||
ocarina_fanfares: # Enable ocarina songs as fanfares. These are longer than usual fanfares. Does nothing without fanfares randomized.
|
||||
false: 50
|
||||
true: 0
|
||||
kokiri_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
kokiri_green: 50
|
||||
goron_red: 0
|
||||
zora_blue: 0
|
||||
black: 0
|
||||
white: 0
|
||||
azure_blue: 0
|
||||
vivid_cyan: 0
|
||||
light_red: 0
|
||||
fuchsia: 0
|
||||
purple: 0
|
||||
majora_purple: 0
|
||||
twitch_purple: 0
|
||||
purple_heart: 0
|
||||
persian_rose: 0
|
||||
dirty_yellow: 0
|
||||
blush_pink: 0
|
||||
hot_pink: 0
|
||||
rose_pink: 0
|
||||
orange: 0
|
||||
gray: 0
|
||||
gold: 0
|
||||
silver: 0
|
||||
beige: 0
|
||||
teal: 0
|
||||
blood_red: 0
|
||||
blood_orange: 0
|
||||
royal_blue: 0
|
||||
sonic_blue: 0
|
||||
nes_green: 0
|
||||
dark_green: 0
|
||||
lumen: 0
|
||||
goron_color: # Choose a color. Uses the same options as "kokiri_color".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
goron_red: 50
|
||||
zora_color: # Choose a color. Uses the same options as "kokiri_color".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
zora_blue: 50
|
||||
silver_gauntlets_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
silver: 50
|
||||
gold: 0
|
||||
black: 0
|
||||
green: 0
|
||||
blue: 0
|
||||
bronze: 0
|
||||
red: 0
|
||||
sky_blue: 0
|
||||
pink: 0
|
||||
magenta: 0
|
||||
orange: 0
|
||||
lime: 0
|
||||
purple: 0
|
||||
golden_gauntlets_color: # Choose a color. Uses the same options as "silver_gauntlets_color".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
gold: 50
|
||||
mirror_shield_frame_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
red: 50
|
||||
green: 0
|
||||
blue: 0
|
||||
yellow: 0
|
||||
cyan: 0
|
||||
magenta: 0
|
||||
orange: 0
|
||||
gold: 0
|
||||
purple: 0
|
||||
pink: 0
|
||||
navi_color_default_inner: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
rainbow: 0
|
||||
gold: 0
|
||||
white: 50
|
||||
green: 0
|
||||
light_blue: 0
|
||||
yellow: 0
|
||||
red: 0
|
||||
magenta: 0
|
||||
black: 0
|
||||
tatl: 0
|
||||
tael: 0
|
||||
fi: 0
|
||||
ciela: 0
|
||||
epona: 0
|
||||
ezlo: 0
|
||||
king_of_red_lions: 0
|
||||
linebeck: 0
|
||||
loftwing: 0
|
||||
midna: 0
|
||||
phantom_zelda: 0
|
||||
navi_color_default_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
match_inner: 50
|
||||
navi_color_enemy_inner: # Choose a color. Uses the same options as "navi_color_default_inner".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
yellow: 50
|
||||
navi_color_enemy_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
match_inner: 50
|
||||
navi_color_npc_inner: # Choose a color. Uses the same options as "navi_color_default_inner".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
light_blue: 50
|
||||
navi_color_npc_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
match_inner: 50
|
||||
navi_color_prop_inner: # Choose a color. Uses the same options as "navi_color_default_inner".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
green: 50
|
||||
navi_color_prop_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
match_inner: 50
|
||||
sword_trail_duration: # Set the duration for sword trails.
|
||||
# you can add additional values between minimum and maximum
|
||||
4: 50 # minimum value
|
||||
20: 0 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
sword_trail_color_inner: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
rainbow: 0
|
||||
white: 50
|
||||
red: 0
|
||||
green: 0
|
||||
blue: 0
|
||||
cyan: 0
|
||||
magenta: 0
|
||||
orange: 0
|
||||
gold: 0
|
||||
purple: 0
|
||||
pink: 0
|
||||
sword_trail_color_outer: # Uses the same options as "sword_trail_color_inner" plus "match_inner".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
match_inner: 50
|
||||
bombchu_trail_color_inner: # Uses the same options as "sword_trail_color_inner".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
red: 50
|
||||
bombchu_trail_color_outer: # Uses the same options as "sword_trail_color_inner" plus "match_inner".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
match_inner: 50
|
||||
boomerang_trail_color_inner: # Uses the same options as "sword_trail_color_inner".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
yellow: 50
|
||||
boomerang_trail_color_outer: # Uses the same options as "sword_trail_color_inner" plus "match_inner".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
match_inner: 50
|
||||
heart_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
red: 50
|
||||
green: 0
|
||||
blue: 0
|
||||
yellow: 0
|
||||
magic_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
green: 50
|
||||
red: 0
|
||||
blue: 0
|
||||
purple: 0
|
||||
pink: 0
|
||||
yellow: 0
|
||||
white: 0
|
||||
a_button_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
n64_blue: 50
|
||||
n64_green: 0
|
||||
n64_red: 0
|
||||
gamecube_green: 0
|
||||
gamecube_red: 0
|
||||
gamecube_grey: 0
|
||||
yellow: 0
|
||||
black: 0
|
||||
white: 0
|
||||
magenta: 0
|
||||
ruby: 0
|
||||
sapphire: 0
|
||||
lime: 0
|
||||
cyan: 0
|
||||
purple: 0
|
||||
orange: 0
|
||||
b_button_color: # Choose a color. Uses the same options as "a_button_color".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
n64_green: 50
|
||||
c_button_color: # Choose a color. Uses the same options as "a_button_color".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
yellow: 50
|
||||
start_button_color: # Choose a color. Uses the same options as "a_button_color".
|
||||
random_choice: 0
|
||||
completely_random: 0
|
||||
n64_red: 50
|
||||
sfx_navi_overworld: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
|
||||
default: 50
|
||||
completely_random: 0
|
||||
random_ear_safe: 0
|
||||
random_choice: 0
|
||||
none: 0
|
||||
sfx_navi_enemy: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
|
||||
default: 50
|
||||
completely_random: 0
|
||||
random_ear_safe: 0
|
||||
random_choice: 0
|
||||
none: 0
|
||||
sfx_low_hp: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
|
||||
default: 50
|
||||
completely_random: 0
|
||||
random_ear_safe: 0
|
||||
random_choice: 0
|
||||
none: 0
|
||||
sfx_menu_cursor: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
|
||||
default: 50
|
||||
completely_random: 0
|
||||
random_ear_safe: 0
|
||||
random_choice: 0
|
||||
none: 0
|
||||
sfx_menu_select: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
|
||||
default: 50
|
||||
completely_random: 0
|
||||
random_ear_safe: 0
|
||||
random_choice: 0
|
||||
none: 0
|
||||
sfx_nightfall: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
|
||||
default: 50
|
||||
completely_random: 0
|
||||
random_ear_safe: 0
|
||||
random_choice: 0
|
||||
none: 0
|
||||
sfx_horse_neigh: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
|
||||
default: 50
|
||||
completely_random: 0
|
||||
random_ear_safe: 0
|
||||
random_choice: 0
|
||||
none: 0
|
||||
sfx_hover_boots: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
|
||||
default: 50
|
||||
completely_random: 0
|
||||
random_ear_safe: 0
|
||||
random_choice: 0
|
||||
sfx_ocarina: # Change the sound of the ocarina.
|
||||
ocarina: 50
|
||||
malon: 0
|
||||
whistle: 0
|
||||
harp: 0
|
||||
grind_organ: 0
|
||||
flute: 0
|
||||
logic_tricks:
|
||||
[]
|
||||
|
||||
# meta_ignore, linked_options and triggers work for any game
|
||||
meta_ignore: # Nullify options specified in the meta.yaml file. Adding an option here guarantees it will not occur in your seed, even if the .yaml file specifies it
|
||||
mode:
|
||||
|
||||
@@ -2,7 +2,7 @@ colorama>=0.4.4
|
||||
websockets>=9.1
|
||||
PyYAML>=5.4.1
|
||||
fuzzywuzzy>=0.18.0
|
||||
prompt_toolkit>=3.0.19
|
||||
prompt_toolkit>=3.0.20
|
||||
appdirs>=1.4.4
|
||||
jinja2>=3.0.1
|
||||
schema>=0.7.4
|
||||
|
||||
3
setup.py
3
setup.py
@@ -162,6 +162,9 @@ if signtool:
|
||||
os.system(signtool + os.path.join(buildfolder, exe.target_name))
|
||||
print(f"Signing SNI")
|
||||
os.system(signtool + os.path.join(buildfolder, "SNI", "SNI.exe"))
|
||||
print(f"Signing OoT Utils")
|
||||
for exe_path in (("Compress", "Compress.exe"), ("Decompress", "Decompress.exe")):
|
||||
os.system(signtool + os.path.join(buildfolder, "lib", "worlds", "oot", "data", *exe_path))
|
||||
|
||||
remove_sprites_from_folder(buildfolder / "data" / "sprites" / "alttpr")
|
||||
|
||||
|
||||
@@ -4,11 +4,10 @@ from BaseClasses import MultiWorld
|
||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||
from worlds.alttp.EntranceShuffle import link_inverted_entrances
|
||||
from worlds.alttp.InvertedRegions import create_inverted_regions
|
||||
from worlds.alttp.ItemPool import generate_itempool, difficulties
|
||||
from worlds.alttp.ItemPool import difficulties
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
from worlds.alttp.Regions import mark_light_world_regions
|
||||
from worlds.alttp.Shops import create_shops
|
||||
from worlds.alttp.Rules import set_rules
|
||||
from test.TestBase import TestBase
|
||||
|
||||
from worlds import AutoWorld
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import unittest
|
||||
from argparse import Namespace
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.alttp.Dungeons import create_dungeons
|
||||
@@ -7,6 +8,7 @@ from worlds.alttp.EntranceShuffle import connect_entrance, Inverted_LW_Entrances
|
||||
from worlds.alttp.InvertedRegions import create_inverted_regions
|
||||
from worlds.alttp.ItemPool import difficulties
|
||||
from worlds.alttp.Rules import set_inverted_big_bomb_rules
|
||||
from worlds import AutoWorld
|
||||
|
||||
|
||||
class TestInvertedBombRules(unittest.TestCase):
|
||||
@@ -14,6 +16,10 @@ class TestInvertedBombRules(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
self.world.mode[1] = "inverted"
|
||||
args = Namespace
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
self.world.set_options(args)
|
||||
self.world.difficulty_requirements[1] = difficulties['normal']
|
||||
create_inverted_regions(self.world, 1)
|
||||
create_dungeons(self.world, 1)
|
||||
|
||||
@@ -78,7 +78,10 @@ class World(metaclass=AutoWorldRegister):
|
||||
# maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"}
|
||||
item_name_groups: Dict[str, Set[str]] = {}
|
||||
|
||||
data_version = 1 # increment this every time something in your world's names/id mappings changes.
|
||||
# increment this every time something in your world's names/id mappings changes.
|
||||
# While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be
|
||||
# retrieved by clients on every connection.
|
||||
data_version = 1
|
||||
|
||||
hint_blacklist: Set[str] = frozenset() # any names that should not be hintable
|
||||
|
||||
@@ -88,6 +91,13 @@ class World(metaclass=AutoWorldRegister):
|
||||
# the client finds its own items in its own world.
|
||||
remote_items: bool = True
|
||||
|
||||
# For games where after a victory it is impossible to go back in and get additional/remaining Locations checked.
|
||||
# this forces forfeit: auto for those games.
|
||||
forced_auto_forfeit: bool = False
|
||||
|
||||
# Hide World Type from various views. Does not remove functionality.
|
||||
hidden = False
|
||||
|
||||
# autoset on creation:
|
||||
world: MultiWorld
|
||||
player: int
|
||||
@@ -127,11 +137,15 @@ class World(metaclass=AutoWorldRegister):
|
||||
pass
|
||||
|
||||
def fill_hook(cls, progitempool: List[Item], nonexcludeditempool: List[Item],
|
||||
localrestitempool: Dict[int, List[Item]], restitempool: List[Item], fill_locations: List[Location]):
|
||||
localrestitempool: Dict[int, List[Item]], nonlocalrestitempool: Dict[int, List[Item]],
|
||||
restitempool: List[Item], fill_locations: List[Location]):
|
||||
"""Special method that gets called as part of distribute_items_restrictive (main fill).
|
||||
This gets called once per present world type."""
|
||||
pass
|
||||
|
||||
def post_fill(self):
|
||||
"""Optional Method that is called after regular fill. Can be used to do adjustments before output generation."""
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
"""This method gets called from a threadpool, do not use world.random here.
|
||||
If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead."""
|
||||
@@ -150,9 +164,10 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
# end of Main.py calls
|
||||
|
||||
def collect_item(self, state: CollectionState, item: Item) -> Optional[str]:
|
||||
def collect_item(self, state: CollectionState, item: Item, remove=False) -> Optional[str]:
|
||||
"""Collect an item name into state. For speed reasons items that aren't logically useful get skipped.
|
||||
Collect None to skip item."""
|
||||
Collect None to skip item.
|
||||
:param remove: indicate if this is meant to remove from state instead of adding."""
|
||||
if item.advancement:
|
||||
return item.name
|
||||
|
||||
@@ -170,7 +185,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
return False
|
||||
|
||||
def remove(self, state: CollectionState, item: Item) -> bool:
|
||||
name = self.collect_item(state, item)
|
||||
name = self.collect_item(state, item, True)
|
||||
if name:
|
||||
state.prog_items[name, item.player] -= 1
|
||||
if state.prog_items[name, item.player] < 1:
|
||||
@@ -178,6 +193,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# any methods attached to this can be used as part of CollectionState,
|
||||
# please use a prefix as all of them get clobbered together
|
||||
class LogicMixin(metaclass=AutoLogicRegister):
|
||||
|
||||
@@ -34,3 +34,7 @@ network_data_package = {
|
||||
"version": sum(world.data_version for world in AutoWorldRegister.world_types.values()),
|
||||
"games": games,
|
||||
}
|
||||
|
||||
# Set entire datapackage to version 0 if any of them are set to 0
|
||||
if any(not world.data_version for world in AutoWorldRegister.world_types.values()):
|
||||
network_data_package["version"] = 0
|
||||
|
||||
@@ -3,12 +3,16 @@ from worlds.alttp.Bosses import BossFactory
|
||||
from Fill import fill_restrictive
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
from worlds.alttp.Regions import lookup_boss_drops
|
||||
from worlds.alttp.Options import smallkey_shuffle
|
||||
|
||||
|
||||
def create_dungeons(world, player):
|
||||
def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
|
||||
dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.keyshuffle[player] == "universal" else small_keys,
|
||||
dungeon = Dungeon(name, dungeon_regions, big_key,
|
||||
[] if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal else small_keys,
|
||||
dungeon_items, player)
|
||||
for item in dungeon.all_items:
|
||||
item.dungeon = dungeon
|
||||
dungeon.boss = BossFactory(default_boss, player) if default_boss else None
|
||||
for region in dungeon.regions:
|
||||
world.get_region(region, player).dungeon = dungeon
|
||||
@@ -21,56 +25,141 @@ def create_dungeons(world, player):
|
||||
EP = make_dungeon('Eastern Palace', 'Armos Knights', ['Eastern Palace'],
|
||||
ItemFactory('Big Key (Eastern Palace)', player), [],
|
||||
ItemFactory(['Map (Eastern Palace)', 'Compass (Eastern Palace)'], player))
|
||||
DP = make_dungeon('Desert Palace', 'Lanmolas', ['Desert Palace North', 'Desert Palace Main (Inner)', 'Desert Palace Main (Outer)', 'Desert Palace East'], ItemFactory('Big Key (Desert Palace)', player), [ItemFactory('Small Key (Desert Palace)', player)], ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)'], player))
|
||||
ToH = make_dungeon('Tower of Hera', 'Moldorm', ['Tower of Hera (Bottom)', 'Tower of Hera (Basement)', 'Tower of Hera (Top)'], ItemFactory('Big Key (Tower of Hera)', player), [ItemFactory('Small Key (Tower of Hera)', player)], ItemFactory(['Map (Tower of Hera)', 'Compass (Tower of Hera)'], player))
|
||||
PoD = make_dungeon('Palace of Darkness', 'Helmasaur King', ['Palace of Darkness (Entrance)', 'Palace of Darkness (Center)', 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness (Bonk Section)', 'Palace of Darkness (North)', 'Palace of Darkness (Maze)', 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness (Final Section)'], ItemFactory('Big Key (Palace of Darkness)', player), ItemFactory(['Small Key (Palace of Darkness)'] * 6, player), ItemFactory(['Map (Palace of Darkness)', 'Compass (Palace of Darkness)'], player))
|
||||
TT = make_dungeon('Thieves Town', 'Blind', ['Thieves Town (Entrance)', 'Thieves Town (Deep)', 'Blind Fight'], ItemFactory('Big Key (Thieves Town)', player), [ItemFactory('Small Key (Thieves Town)', player)], ItemFactory(['Map (Thieves Town)', 'Compass (Thieves Town)'], player))
|
||||
SW = make_dungeon('Skull Woods', 'Mothula', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section', 'Skull Woods Second Section', 'Skull Woods Second Section (Drop)', 'Skull Woods Final Section (Mothula)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'], ItemFactory('Big Key (Skull Woods)', player), ItemFactory(['Small Key (Skull Woods)'] * 3, player), ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)'], player))
|
||||
SP = make_dungeon('Swamp Palace', 'Arrghus', ['Swamp Palace (Entrance)', 'Swamp Palace (First Room)', 'Swamp Palace (Starting Area)', 'Swamp Palace (Center)', 'Swamp Palace (North)'], ItemFactory('Big Key (Swamp Palace)', player), [ItemFactory('Small Key (Swamp Palace)', player)], ItemFactory(['Map (Swamp Palace)', 'Compass (Swamp Palace)'], player))
|
||||
IP = make_dungeon('Ice Palace', 'Kholdstare', ['Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)', 'Ice Palace (East Top)', 'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)', player), ItemFactory(['Small Key (Ice Palace)'] * 2, player), ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)'], player))
|
||||
MM = make_dungeon('Misery Mire', 'Vitreous', ['Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)', 'Misery Mire (Vitreous)'], ItemFactory('Big Key (Misery Mire)', player), ItemFactory(['Small Key (Misery Mire)'] * 3, player), ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player))
|
||||
TR = make_dungeon('Turtle Rock', 'Trinexx', ['Turtle Rock (Entrance)', 'Turtle Rock (First Section)', 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Turtle Rock (Crystaroller Room)', 'Turtle Rock (Dark Room)', 'Turtle Rock (Eye Bridge)', 'Turtle Rock (Trinexx)'], ItemFactory('Big Key (Turtle Rock)', player), ItemFactory(['Small Key (Turtle Rock)'] * 4, player), ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player))
|
||||
DP = make_dungeon('Desert Palace', 'Lanmolas',
|
||||
['Desert Palace North', 'Desert Palace Main (Inner)', 'Desert Palace Main (Outer)',
|
||||
'Desert Palace East'], ItemFactory('Big Key (Desert Palace)', player),
|
||||
[ItemFactory('Small Key (Desert Palace)', player)],
|
||||
ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)'], player))
|
||||
ToH = make_dungeon('Tower of Hera', 'Moldorm',
|
||||
['Tower of Hera (Bottom)', 'Tower of Hera (Basement)', 'Tower of Hera (Top)'],
|
||||
ItemFactory('Big Key (Tower of Hera)', player),
|
||||
[ItemFactory('Small Key (Tower of Hera)', player)],
|
||||
ItemFactory(['Map (Tower of Hera)', 'Compass (Tower of Hera)'], player))
|
||||
PoD = make_dungeon('Palace of Darkness', 'Helmasaur King',
|
||||
['Palace of Darkness (Entrance)', 'Palace of Darkness (Center)',
|
||||
'Palace of Darkness (Big Key Chest)', 'Palace of Darkness (Bonk Section)',
|
||||
'Palace of Darkness (North)', 'Palace of Darkness (Maze)',
|
||||
'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness (Final Section)'],
|
||||
ItemFactory('Big Key (Palace of Darkness)', player),
|
||||
ItemFactory(['Small Key (Palace of Darkness)'] * 6, player),
|
||||
ItemFactory(['Map (Palace of Darkness)', 'Compass (Palace of Darkness)'], player))
|
||||
TT = make_dungeon('Thieves Town', 'Blind', ['Thieves Town (Entrance)', 'Thieves Town (Deep)', 'Blind Fight'],
|
||||
ItemFactory('Big Key (Thieves Town)', player), [ItemFactory('Small Key (Thieves Town)', player)],
|
||||
ItemFactory(['Map (Thieves Town)', 'Compass (Thieves Town)'], player))
|
||||
SW = make_dungeon('Skull Woods', 'Mothula', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section',
|
||||
'Skull Woods Second Section', 'Skull Woods Second Section (Drop)',
|
||||
'Skull Woods Final Section (Mothula)',
|
||||
'Skull Woods First Section (Right)',
|
||||
'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'],
|
||||
ItemFactory('Big Key (Skull Woods)', player),
|
||||
ItemFactory(['Small Key (Skull Woods)'] * 3, player),
|
||||
ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)'], player))
|
||||
SP = make_dungeon('Swamp Palace', 'Arrghus',
|
||||
['Swamp Palace (Entrance)', 'Swamp Palace (First Room)', 'Swamp Palace (Starting Area)',
|
||||
'Swamp Palace (Center)', 'Swamp Palace (North)'], ItemFactory('Big Key (Swamp Palace)', player),
|
||||
[ItemFactory('Small Key (Swamp Palace)', player)],
|
||||
ItemFactory(['Map (Swamp Palace)', 'Compass (Swamp Palace)'], player))
|
||||
IP = make_dungeon('Ice Palace', 'Kholdstare',
|
||||
['Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)', 'Ice Palace (East Top)',
|
||||
'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)', player),
|
||||
ItemFactory(['Small Key (Ice Palace)'] * 2, player),
|
||||
ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)'], player))
|
||||
MM = make_dungeon('Misery Mire', 'Vitreous',
|
||||
['Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)',
|
||||
'Misery Mire (Vitreous)'], ItemFactory('Big Key (Misery Mire)', player),
|
||||
ItemFactory(['Small Key (Misery Mire)'] * 3, player),
|
||||
ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player))
|
||||
TR = make_dungeon('Turtle Rock', 'Trinexx',
|
||||
['Turtle Rock (Entrance)', 'Turtle Rock (First Section)', 'Turtle Rock (Chain Chomp Room)',
|
||||
'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Turtle Rock (Crystaroller Room)',
|
||||
'Turtle Rock (Dark Room)', 'Turtle Rock (Eye Bridge)', 'Turtle Rock (Trinexx)'],
|
||||
ItemFactory('Big Key (Turtle Rock)', player),
|
||||
ItemFactory(['Small Key (Turtle Rock)'] * 4, player),
|
||||
ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player))
|
||||
|
||||
if world.mode[player] != 'inverted':
|
||||
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
|
||||
GT = make_dungeon('Ganons Tower', 'Agahnim2', ['Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player), ItemFactory(['Small Key (Ganons Tower)'] * 4, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
|
||||
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None,
|
||||
ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
|
||||
GT = make_dungeon('Ganons Tower', 'Agahnim2',
|
||||
['Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)',
|
||||
'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)',
|
||||
'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)',
|
||||
'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'],
|
||||
ItemFactory('Big Key (Ganons Tower)', player),
|
||||
ItemFactory(['Small Key (Ganons Tower)'] * 4, player),
|
||||
ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
|
||||
else:
|
||||
AT = make_dungeon('Inverted Agahnims Tower', 'Agahnim', ['Inverted Agahnims Tower', 'Agahnim 1'], None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
|
||||
GT = make_dungeon('Inverted Ganons Tower', 'Agahnim2', ['Inverted Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player), ItemFactory(['Small Key (Ganons Tower)'] * 4, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
|
||||
AT = make_dungeon('Inverted Agahnims Tower', 'Agahnim', ['Inverted Agahnims Tower', 'Agahnim 1'], None,
|
||||
ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
|
||||
GT = make_dungeon('Inverted Ganons Tower', 'Agahnim2',
|
||||
['Inverted Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)',
|
||||
'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)',
|
||||
'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)',
|
||||
'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)',
|
||||
'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player),
|
||||
ItemFactory(['Small Key (Ganons Tower)'] * 4, player),
|
||||
ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
|
||||
|
||||
GT.bosses['bottom'] = BossFactory('Armos Knights', player)
|
||||
GT.bosses['middle'] = BossFactory('Lanmolas', player)
|
||||
GT.bosses['top'] = BossFactory('Moldorm', player)
|
||||
|
||||
world.dungeons += [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT]
|
||||
for dungeon in [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT]:
|
||||
world.dungeons[dungeon.name, dungeon.player] = dungeon
|
||||
|
||||
|
||||
def get_dungeon_item_pool(world):
|
||||
items = [item for dungeon in world.dungeons for item in dungeon.all_items]
|
||||
items = [item for dungeon in world.dungeons.values() for item in dungeon.all_items]
|
||||
for item in items:
|
||||
item.world = world
|
||||
return items
|
||||
|
||||
|
||||
def fill_dungeons_restrictive(world):
|
||||
"""Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside."""
|
||||
dungeon_items = [item for item in get_dungeon_item_pool(world) if
|
||||
(((item.smallkey and not world.keyshuffle[item.player])
|
||||
or (item.bigkey and not world.bigkeyshuffle[item.player])
|
||||
or (item.map and not world.mapshuffle[item.player])
|
||||
or (item.compass and not world.compassshuffle[item.player])
|
||||
) and world.goal[item.player] != 'icerodhunt')]
|
||||
if dungeon_items:
|
||||
restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if restricted}
|
||||
locations = [location for location in world.get_unfilled_dungeon_locations()
|
||||
if not (location.player in restricted_players and location.name in lookup_boss_drops)] # filter boss
|
||||
def get_dungeon_item_pool_player(world, player):
|
||||
items = [item for dungeon in world.dungeons.values() if dungeon.player == player for item in dungeon.all_items]
|
||||
for item in items:
|
||||
item.world = world
|
||||
return items
|
||||
|
||||
world.random.shuffle(locations)
|
||||
all_state_base = world.get_all_state()
|
||||
# sort in the order Big Key, Small Key, Other before placing dungeon items
|
||||
sort_order = {"BigKey": 3, "SmallKey": 2}
|
||||
dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1))
|
||||
fill_restrictive(world, all_state_base, locations, dungeon_items, True, True)
|
||||
|
||||
def fill_dungeons_restrictive(autoworld, world):
|
||||
"""Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside."""
|
||||
localized: set = set()
|
||||
dungeon_specific: set = set()
|
||||
for subworld in world.get_game_worlds("A Link to the Past"):
|
||||
player = subworld.player
|
||||
localized |= {(player, item_name) for item_name in
|
||||
subworld.dungeon_local_item_names}
|
||||
dungeon_specific |= {(player, item_name) for item_name in
|
||||
subworld.dungeon_specific_item_names}
|
||||
|
||||
if localized:
|
||||
in_dungeon_items = [item for item in get_dungeon_item_pool(world) if (item.player, item.name) in localized]
|
||||
if in_dungeon_items:
|
||||
restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if
|
||||
restricted}
|
||||
locations = [location for location in world.get_unfilled_dungeon_locations()
|
||||
# filter boss
|
||||
if not (location.player in restricted_players and location.name in lookup_boss_drops)]
|
||||
if dungeon_specific:
|
||||
for location in locations:
|
||||
dungeon = location.parent_region.dungeon
|
||||
orig_rule = location.item_rule
|
||||
location.item_rule = lambda item, dungeon=dungeon, orig_rule=orig_rule: \
|
||||
(not (item.player, item.name) in dungeon_specific or item.dungeon is dungeon) and orig_rule(item)
|
||||
|
||||
world.random.shuffle(locations)
|
||||
all_state_base = world.get_all_state(use_cache=True)
|
||||
# Dungeon-locked items have to be placed first, to not run out of spaces for dungeon-locked items
|
||||
# subsort in the order Big Key, Small Key, Other before placing dungeon items
|
||||
|
||||
sort_order = {"BigKey": 3, "SmallKey": 2}
|
||||
in_dungeon_items.sort(
|
||||
key=lambda item: sort_order.get(item.type, 1) +
|
||||
(5 if (item.player, item.name) in dungeon_specific else 0))
|
||||
for item in in_dungeon_items:
|
||||
all_state_base.remove(item)
|
||||
fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True)
|
||||
|
||||
|
||||
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
|
||||
@@ -79,7 +168,8 @@ dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
|
||||
'Palace of Darkness - Prize': [0x155B8],
|
||||
'Swamp Palace - Prize': [0x155B7],
|
||||
'Thieves\' Town - Prize': [0x155C6],
|
||||
'Skull Woods - Prize': [0x155BA, 0x155BB, 0x155BC, 0x155BD, 0x15608, 0x15609, 0x1560A, 0x1560B],
|
||||
'Skull Woods - Prize': [0x155BA, 0x155BB, 0x155BC, 0x155BD, 0x15608, 0x15609, 0x1560A,
|
||||
0x1560B],
|
||||
'Ice Palace - Prize': [0x155BF],
|
||||
'Misery Mire - Prize': [0x155B9],
|
||||
'Turtle Rock - Prize': [0x155C7, 0x155A7, 0x155AA, 0x155AB]}
|
||||
|
||||
@@ -206,26 +206,10 @@ def parse_arguments(argv, no_defaults=False):
|
||||
time).
|
||||
''', type=int)
|
||||
|
||||
parser.add_argument('--mapshuffle', default=defval(False),
|
||||
help='Maps are no longer restricted to their dungeons, but can be anywhere',
|
||||
action='store_true')
|
||||
parser.add_argument('--compassshuffle', default=defval(False),
|
||||
help='Compasses are no longer restricted to their dungeons, but can be anywhere',
|
||||
action='store_true')
|
||||
parser.add_argument('--keyshuffle', default=defval("off"), help='\
|
||||
on: Small Keys are no longer restricted to their dungeons, but can be anywhere.\
|
||||
universal: Makes all Small Keys usable in any dungeon and places shops to buy more keys.',
|
||||
choices=["on", "universal", "off"])
|
||||
parser.add_argument('--bigkeyshuffle', default=defval(False),
|
||||
help='Big Keys are no longer restricted to their dungeons, but can be anywhere',
|
||||
action='store_true')
|
||||
parser.add_argument('--keysanity', default=defval(False), help=argparse.SUPPRESS, action='store_true')
|
||||
parser.add_argument('--retro', default=defval(False), help='''\
|
||||
Keys are universal, shooting arrows costs rupees,
|
||||
and a few other little things make this more like Zelda-1.
|
||||
''', action='store_true')
|
||||
parser.add_argument('--startinventory', default=defval(''),
|
||||
help='Specifies a list of items that will be in your starting inventory (separated by commas)')
|
||||
parser.add_argument('--local_items', default=defval(''),
|
||||
help='Specifies a list of items that will not spread across the multiworld (separated by commas)')
|
||||
parser.add_argument('--non_local_items', default=defval(''),
|
||||
@@ -291,13 +275,10 @@ def parse_arguments(argv, no_defaults=False):
|
||||
parser.add_argument('--restrict_dungeon_item_on_boss', default=defval(False), action="store_true")
|
||||
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
|
||||
parser.add_argument('--names', default=defval(''))
|
||||
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
|
||||
parser.add_argument('--outputpath')
|
||||
parser.add_argument('--game', default="A Link to the Past")
|
||||
parser.add_argument('--race', default=defval(False), action='store_true')
|
||||
parser.add_argument('--outputname')
|
||||
parser.add_argument('--disable_glitch_boots', default=defval(False), action='store_true', help='''\
|
||||
turns off starting with Pegasus Boots in glitched modes.''')
|
||||
parser.add_argument('--start_hints')
|
||||
if multiargs.multi:
|
||||
for player in range(1, multiargs.multi + 1):
|
||||
@@ -314,7 +295,6 @@ def parse_arguments(argv, no_defaults=False):
|
||||
ret.plando_connections = []
|
||||
ret.er_seeds = {}
|
||||
|
||||
ret.glitch_boots = not ret.disable_glitch_boots
|
||||
if ret.timer == "none":
|
||||
ret.timer = False
|
||||
if ret.dungeon_counters == 'on':
|
||||
@@ -322,12 +302,6 @@ def parse_arguments(argv, no_defaults=False):
|
||||
elif ret.dungeon_counters == 'off':
|
||||
ret.dungeon_counters = False
|
||||
|
||||
if ret.keysanity:
|
||||
ret.mapshuffle = ret.compassshuffle = ret.keyshuffle = ret.bigkeyshuffle = True
|
||||
elif ret.keyshuffle == "on":
|
||||
ret.keyshuffle = True
|
||||
elif ret.keyshuffle == "off":
|
||||
ret.keyshuffle = False
|
||||
if multiargs.multi:
|
||||
defaults = copy.deepcopy(ret)
|
||||
for player in range(1, multiargs.multi + 1):
|
||||
@@ -336,7 +310,6 @@ def parse_arguments(argv, no_defaults=False):
|
||||
for name in ['logic', 'mode', 'swordless', 'goal', 'difficulty', 'item_functionality',
|
||||
'shuffle', 'open_pyramid', 'timer',
|
||||
'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time',
|
||||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
||||
'local_items', 'non_local_items', 'retro', 'accessibility', 'hints', 'beemizer',
|
||||
'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',
|
||||
'sprite',
|
||||
@@ -344,7 +317,7 @@ def parse_arguments(argv, no_defaults=False):
|
||||
"triforce_pieces_required", "shop_shuffle",
|
||||
"required_medallions", "start_hints",
|
||||
"plando_items", "plando_texts", "plando_connections", "er_seeds",
|
||||
'dungeon_counters', 'glitch_boots', 'killable_thieves',
|
||||
'dungeon_counters', 'killable_thieves',
|
||||
'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
|
||||
'restrict_dungeon_item_on_boss', 'game']:
|
||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave.
|
||||
from collections import defaultdict
|
||||
from worlds.alttp.OverworldGlitchRules import overworld_glitch_connections
|
||||
from worlds.alttp.UnderworldGlitchRules import underworld_glitch_connections
|
||||
|
||||
def link_entrances(world, player):
|
||||
@@ -1066,9 +1067,11 @@ def link_entrances(world, player):
|
||||
raise NotImplementedError(
|
||||
f'{world.shuffle[player]} Shuffling not supported yet. Player {world.get_player_name(player)}')
|
||||
|
||||
# mandatory hybrid major glitches connections
|
||||
if world.logic[player] in ['hybridglitches', 'nologic']:
|
||||
underworld_glitch_connections(world, player)
|
||||
if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']:
|
||||
overworld_glitch_connections(world, player)
|
||||
# mandatory hybrid major glitches connections
|
||||
if world.logic[player] in ['hybridglitches', 'nologic']:
|
||||
underworld_glitch_connections(world, player)
|
||||
|
||||
# check for swamp palace fix
|
||||
if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)':
|
||||
@@ -1771,9 +1774,11 @@ def link_inverted_entrances(world, player):
|
||||
else:
|
||||
raise NotImplementedError('Shuffling not supported yet')
|
||||
|
||||
# mandatory hybrid major glitches connections
|
||||
if world.logic[player] in ['hybridglitches', 'nologic']:
|
||||
underworld_glitch_connections(world, player)
|
||||
if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']:
|
||||
overworld_glitch_connections(world, player)
|
||||
# mandatory hybrid major glitches connections
|
||||
if world.logic[player] in ['hybridglitches', 'nologic']:
|
||||
underworld_glitch_connections(world, player)
|
||||
|
||||
# patch swamp drain
|
||||
if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)':
|
||||
|
||||
@@ -5,10 +5,11 @@ from BaseClasses import Region, RegionType
|
||||
from worlds.alttp.SubClasses import ALttPLocation
|
||||
from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops
|
||||
from worlds.alttp.Bosses import place_bosses
|
||||
from worlds.alttp.Dungeons import get_dungeon_item_pool
|
||||
from worlds.alttp.Dungeons import get_dungeon_item_pool_player
|
||||
from worlds.alttp.EntranceShuffle import connect_entrance
|
||||
from Fill import FillError, fill_restrictive
|
||||
from Fill import FillError
|
||||
from worlds.alttp.Items import ItemFactory, GetBeemizerItem
|
||||
from worlds.alttp.Options import smallkey_shuffle
|
||||
|
||||
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
|
||||
# Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
|
||||
@@ -274,7 +275,7 @@ def generate_itempool(world):
|
||||
itempool.extend(['Rupees (300)'] * 34)
|
||||
itempool.extend(['Bombs (10)'] * 5)
|
||||
itempool.extend(['Arrows (10)'] * 7)
|
||||
if world.keyshuffle[player] == 'universal':
|
||||
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||
itempool.extend(itemdiff.universal_keys)
|
||||
itempool.append('Small Key (Universal)')
|
||||
|
||||
@@ -362,12 +363,8 @@ def generate_itempool(world):
|
||||
if treasure_hunt_icon is not None:
|
||||
world.treasure_hunt_icon[player] = treasure_hunt_icon
|
||||
|
||||
dungeon_items = [item for item in get_dungeon_item_pool(world) if item.player == player
|
||||
and ((item.smallkey and world.keyshuffle[player])
|
||||
or (item.bigkey and world.bigkeyshuffle[player])
|
||||
or (item.map and world.mapshuffle[player])
|
||||
or (item.compass and world.compassshuffle[player])
|
||||
or world.goal[player] == 'icerodhunt')]
|
||||
dungeon_items = [item for item in get_dungeon_item_pool_player(world, player)
|
||||
if item.name not in world.worlds[player].dungeon_local_item_names]
|
||||
|
||||
if world.goal[player] == 'icerodhunt':
|
||||
for item in dungeon_items:
|
||||
@@ -500,14 +497,14 @@ def create_dynamic_shop_locations(world, player):
|
||||
if item is None:
|
||||
continue
|
||||
if item['create_location']:
|
||||
loc = ALttPLocation(player, "{} Slot {}".format(shop.region.name, i + 1), parent=shop.region)
|
||||
loc = ALttPLocation(player, f"{shop.region.name} {shop.slot_names[i]}", parent=shop.region)
|
||||
shop.region.locations.append(loc)
|
||||
world.dynamic_locations.append(loc)
|
||||
|
||||
world.clear_location_cache()
|
||||
|
||||
world.push_item(loc, ItemFactory(item['item'], player), False)
|
||||
loc.shop_slot = True
|
||||
loc.shop_slot = i
|
||||
loc.event = True
|
||||
loc.locked = True
|
||||
|
||||
@@ -637,7 +634,7 @@ def get_pool_core(world, player: int):
|
||||
if retro:
|
||||
replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)'}
|
||||
pool = ['Rupees (5)' if item in replace else item for item in pool]
|
||||
if world.keyshuffle[player] == "universal":
|
||||
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||
pool.extend(diff.universal_keys)
|
||||
item_to_place = 'Small Key (Universal)' if goal != 'icerodhunt' else 'Nothing'
|
||||
if mode == 'standard':
|
||||
@@ -774,7 +771,7 @@ def make_custom_item_pool(world, player):
|
||||
itemtotal = itemtotal + 1
|
||||
|
||||
if mode == 'standard':
|
||||
if world.keyshuffle[player] == "universal":
|
||||
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||
key_location = world.random.choice(
|
||||
['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
|
||||
'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
|
||||
@@ -797,7 +794,7 @@ def make_custom_item_pool(world, player):
|
||||
pool.extend(['Magic Mirror'] * customitemarray[22])
|
||||
pool.extend(['Moon Pearl'] * customitemarray[28])
|
||||
|
||||
if world.keyshuffle == "universal":
|
||||
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||
itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode
|
||||
if itemtotal < total_items_to_place:
|
||||
pool.extend(['Nothing'] * (total_items_to_place - itemtotal))
|
||||
|
||||
@@ -28,6 +28,46 @@ class Goal(Choice):
|
||||
option_hand_in = 2
|
||||
|
||||
|
||||
class DungeonItem(Choice):
|
||||
value: int
|
||||
option_original_dungeon = 0
|
||||
option_own_dungeons = 1
|
||||
option_own_world = 2
|
||||
option_any_world = 3
|
||||
option_different_world = 4
|
||||
alias_true = 3
|
||||
alias_false = 0
|
||||
|
||||
@property
|
||||
def in_dungeon(self):
|
||||
return self.value in {0, 1}
|
||||
|
||||
|
||||
class bigkey_shuffle(DungeonItem):
|
||||
"""Big Key Placement"""
|
||||
item_name_group = "Big Keys"
|
||||
displayname = "Big Key Shuffle"
|
||||
|
||||
|
||||
class smallkey_shuffle(DungeonItem):
|
||||
"""Small Key Placement"""
|
||||
option_universal = 5
|
||||
item_name_group = "Small Keys"
|
||||
displayname = "Small Key Shuffle"
|
||||
|
||||
|
||||
class compass_shuffle(DungeonItem):
|
||||
"""Compass Placement"""
|
||||
item_name_group = "Compasses"
|
||||
displayname = "Compass Shuffle"
|
||||
|
||||
|
||||
class map_shuffle(DungeonItem):
|
||||
"""Map Placement"""
|
||||
item_name_group = "Maps"
|
||||
displayname = "Map Shuffle"
|
||||
|
||||
|
||||
class Crystals(Range):
|
||||
range_start = 0
|
||||
range_end = 7
|
||||
@@ -85,6 +125,7 @@ class Progressive(Choice):
|
||||
def want_progressives(self, random):
|
||||
return random.choice([True, False]) if self.value == self.option_grouped_random else bool(self.value)
|
||||
|
||||
|
||||
class Palette(Choice):
|
||||
option_default = 0
|
||||
option_good = 1
|
||||
@@ -126,9 +167,10 @@ class HeartBeep(Choice):
|
||||
displayname = "Heart Beep Rate"
|
||||
option_normal = 0
|
||||
option_double = 1
|
||||
option_half = 2,
|
||||
option_half = 2
|
||||
option_quarter = 3
|
||||
option_off = 4
|
||||
alias_false = 4
|
||||
|
||||
|
||||
class HeartColor(Choice):
|
||||
@@ -145,6 +187,7 @@ class HeartColor(Choice):
|
||||
return cls(random.randint(0, 3))
|
||||
return super(HeartColor, cls).from_text(text)
|
||||
|
||||
|
||||
class QuickSwap(DefaultOnToggle):
|
||||
displayname = "L/R Quickswapping"
|
||||
|
||||
@@ -162,9 +205,11 @@ class MenuSpeed(Choice):
|
||||
class Music(DefaultOnToggle):
|
||||
displayname = "Play music"
|
||||
|
||||
|
||||
class ReduceFlashing(DefaultOnToggle):
|
||||
displayname = "Reduce Screen Flashes"
|
||||
|
||||
|
||||
class TriforceHud(Choice):
|
||||
displayname = "Display Method for Triforce Hunt"
|
||||
option_normal = 0
|
||||
@@ -172,9 +217,14 @@ class TriforceHud(Choice):
|
||||
option_hide_required = 2
|
||||
option_hide_both = 3
|
||||
|
||||
|
||||
alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"crystals_needed_for_gt": CrystalsTower,
|
||||
"crystals_needed_for_ganon": CrystalsGanon,
|
||||
"bigkey_shuffle": bigkey_shuffle,
|
||||
"smallkey_shuffle": smallkey_shuffle,
|
||||
"compass_shuffle": compass_shuffle,
|
||||
"map_shuffle": map_shuffle,
|
||||
"progressive": Progressive,
|
||||
"shop_item_slots": ShopItemSlots,
|
||||
"ow_palettes": OWPalette,
|
||||
@@ -189,6 +239,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"menuspeed": MenuSpeed,
|
||||
"music": Music,
|
||||
"reduceflashing": ReduceFlashing,
|
||||
"triforcehud": TriforceHud
|
||||
"triforcehud": TriforceHud,
|
||||
"glitch_boots": DefaultOnToggle
|
||||
|
||||
}
|
||||
|
||||
@@ -235,24 +235,41 @@ def no_logic_rules(world, player):
|
||||
create_no_logic_connections(player, world, get_mirror_offset_spots_lw(player))
|
||||
|
||||
|
||||
def overworld_glitch_connections(world, player):
|
||||
|
||||
# Boots-accessible locations.
|
||||
create_owg_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'))
|
||||
create_owg_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player))
|
||||
|
||||
# Glitched speed drops.
|
||||
create_owg_connections(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'))
|
||||
|
||||
# Mirror clip spots.
|
||||
if world.mode[player] != 'inverted':
|
||||
create_owg_connections(player, world, get_mirror_clip_spots_dw())
|
||||
create_owg_connections(player, world, get_mirror_offset_spots_dw())
|
||||
else:
|
||||
create_owg_connections(player, world, get_mirror_offset_spots_lw(player))
|
||||
|
||||
|
||||
def overworld_glitches_rules(world, player):
|
||||
|
||||
# Boots-accessible locations.
|
||||
create_owg_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: state.can_boots_clip_lw(player))
|
||||
create_owg_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: state.can_boots_clip_dw(player))
|
||||
set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: state.can_boots_clip_lw(player))
|
||||
set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: state.can_boots_clip_dw(player))
|
||||
|
||||
# Glitched speed drops.
|
||||
create_owg_connections(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: state.can_get_glitched_speed_dw(player))
|
||||
set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: state.can_get_glitched_speed_dw(player))
|
||||
# Dark Death Mountain Ledge Clip Spot also accessible with mirror.
|
||||
if world.mode[player] != 'inverted':
|
||||
add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
|
||||
# Mirror clip spots.
|
||||
if world.mode[player] != 'inverted':
|
||||
create_owg_connections(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player))
|
||||
create_owg_connections(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_lw(player))
|
||||
set_owg_connection_rules(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player))
|
||||
set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_lw(player))
|
||||
else:
|
||||
create_owg_connections(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_dw(player))
|
||||
set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_dw(player))
|
||||
|
||||
# Regions that require the boots and some other stuff.
|
||||
if world.mode[player] != 'inverted':
|
||||
@@ -282,12 +299,16 @@ def create_no_logic_connections(player, world, connections):
|
||||
parent.exits.append(connection)
|
||||
connection.connect(target)
|
||||
|
||||
def create_owg_connections(player, world, connections, default_rule):
|
||||
def create_owg_connections(player, world, connections):
|
||||
for entrance, parent_region, target_region, *rule_override in connections:
|
||||
parent = world.get_region(parent_region, player)
|
||||
target = world.get_region(target_region, player)
|
||||
connection = Entrance(player, entrance, parent)
|
||||
parent.exits.append(connection)
|
||||
connection.connect(target)
|
||||
|
||||
def set_owg_connection_rules(player, world, connections, default_rule):
|
||||
for entrance, _, _, *rule_override in connections:
|
||||
connection = world.get_entrance(entrance, player)
|
||||
rule = rule_override[0] if len(rule_override) > 0 else default_rule
|
||||
connection.access_rule = rule
|
||||
|
||||
@@ -676,12 +676,10 @@ location_table: typing.Dict[str,
|
||||
|
||||
from worlds.alttp.Shops import shop_table_by_location_id, shop_table_by_location
|
||||
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
|
||||
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()},
|
||||
-1: "Cheat Console", -2: "Server"}
|
||||
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}}
|
||||
lookup_id_to_name.update(shop_table_by_location_id)
|
||||
lookup_name_to_id = {name: data[0] for name, data in location_table.items() if type(data[0]) == int}
|
||||
lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()},
|
||||
"Cheat Console": -1, "Server": -2}
|
||||
lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()}}
|
||||
lookup_name_to_id.update(shop_table_by_location)
|
||||
|
||||
lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 191256: 'Kings Grave Inner Rocks',
|
||||
|
||||
@@ -37,6 +37,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts
|
||||
from Utils import local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen
|
||||
from worlds.alttp.Items import ItemFactory, item_table
|
||||
from worlds.alttp.EntranceShuffle import door_addresses
|
||||
from worlds.alttp.Options import smallkey_shuffle
|
||||
import Patch
|
||||
|
||||
try:
|
||||
@@ -763,7 +764,7 @@ def patch_rom(world, rom, player, enemized):
|
||||
# patch items
|
||||
|
||||
for location in world.get_locations():
|
||||
if location.player != player or location.address is None or location.shop_slot:
|
||||
if location.player != player or location.address is None or location.shop_slot is not None:
|
||||
continue
|
||||
|
||||
itemid = location.item.code if location.item is not None else 0x5A
|
||||
@@ -802,14 +803,14 @@ def patch_rom(world, rom, player, enemized):
|
||||
|
||||
# patch music
|
||||
music_addresses = dungeon_music_addresses[location.name]
|
||||
if world.mapshuffle[player]:
|
||||
if world.map_shuffle[player]:
|
||||
music = local_random.choice([0x11, 0x16])
|
||||
else:
|
||||
music = 0x11 if 'Pendant' in location.item.name else 0x16
|
||||
for music_address in music_addresses:
|
||||
rom.write_byte(music_address, music)
|
||||
|
||||
if world.mapshuffle[player]:
|
||||
if world.map_shuffle[player]:
|
||||
rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
|
||||
|
||||
# patch entrance/exits/holes
|
||||
@@ -1491,18 +1492,18 @@ def patch_rom(world, rom, player, enemized):
|
||||
# block HC upstairs doors in rain state in standard mode
|
||||
rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" and world.shuffle[player] != 'vanilla' else 0x00)
|
||||
|
||||
rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle[player] is True else 0x00)
|
||||
| (0x02 if world.compassshuffle[player] else 0x00)
|
||||
| (0x04 if world.mapshuffle[player] else 0x00)
|
||||
| (0x08 if world.bigkeyshuffle[player] else 0x00))) # free roaming item text boxes
|
||||
rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] else 0x00) # maps showing crystals on overworld
|
||||
rom.write_byte(0x18016A, 0x10 | ((0x01 if world.smallkey_shuffle[player] else 0x00)
|
||||
| (0x02 if world.compass_shuffle[player] else 0x00)
|
||||
| (0x04 if world.map_shuffle[player] else 0x00)
|
||||
| (0x08 if world.bigkey_shuffle[player] else 0x00))) # free roaming item text boxes
|
||||
rom.write_byte(0x18003B, 0x01 if world.map_shuffle[player] else 0x00) # maps showing crystals on overworld
|
||||
|
||||
# compasses showing dungeon count
|
||||
if world.clock_mode[player] or not world.dungeon_counters[player]:
|
||||
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
|
||||
elif world.dungeon_counters[player] is True:
|
||||
rom.write_byte(0x18003C, 0x02) # always on
|
||||
elif world.compassshuffle[player] or world.dungeon_counters[player] == 'pickup':
|
||||
elif world.compass_shuffle[player] or world.dungeon_counters[player] == 'pickup':
|
||||
rom.write_byte(0x18003C, 0x01) # show on pickup
|
||||
else:
|
||||
rom.write_byte(0x18003C, 0x00)
|
||||
@@ -1515,10 +1516,11 @@ def patch_rom(world, rom, player, enemized):
|
||||
# b - Big Key
|
||||
# a - Small Key
|
||||
#
|
||||
rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] is True else 0x00)
|
||||
| (0x02 if world.bigkeyshuffle[player] else 0x00)
|
||||
| (0x04 if world.mapshuffle[player] else 0x00)
|
||||
| (0x08 if world.compassshuffle[player] else 0x00))) # free roaming items in menu
|
||||
rom.write_byte(0x180045, ((0x00 if (world.smallkey_shuffle[player] == smallkey_shuffle.option_original_dungeon or
|
||||
world.smallkey_shuffle[player] == smallkey_shuffle.option_universal) else 0x01)
|
||||
| (0x02 if world.bigkey_shuffle[player] else 0x00)
|
||||
| (0x04 if world.map_shuffle[player] else 0x00)
|
||||
| (0x08 if world.compass_shuffle[player] else 0x00))) # free roaming items in menu
|
||||
|
||||
# Map reveals
|
||||
reveal_bytes = {
|
||||
@@ -1544,11 +1546,11 @@ def patch_rom(world, rom, player, enemized):
|
||||
return 0x0000
|
||||
|
||||
rom.write_int16(0x18017A,
|
||||
get_reveal_bytes('Green Pendant') if world.mapshuffle[player] else 0x0000) # Sahasrahla reveal
|
||||
rom.write_int16(0x18017C, get_reveal_bytes('Crystal 5') | get_reveal_bytes('Crystal 6') if world.mapshuffle[
|
||||
get_reveal_bytes('Green Pendant') if world.map_shuffle[player] else 0x0000) # Sahasrahla reveal
|
||||
rom.write_int16(0x18017C, get_reveal_bytes('Crystal 5') | get_reveal_bytes('Crystal 6') if world.map_shuffle[
|
||||
player] else 0x0000) # Bomb Shop Reveal
|
||||
|
||||
rom.write_byte(0x180172, 0x01 if world.keyshuffle[player] == "universal" else 0x00) # universal keys
|
||||
rom.write_byte(0x180172, 0x01 if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal else 0x00) # universal keys
|
||||
rom.write_byte(0x18637E, 0x01 if world.retro[player] else 0x00) # Skip quiver in item shops once bought
|
||||
rom.write_byte(0x180175, 0x01 if world.retro[player] else 0x00) # rupee bow
|
||||
rom.write_byte(0x180176, 0x0A if world.retro[player] else 0x00) # wood arrow cost
|
||||
@@ -2087,9 +2089,9 @@ def write_strings(rom, world, player):
|
||||
if not dest:
|
||||
return "nothing"
|
||||
if ped_hint:
|
||||
hint = dest.pedestal_hint_text if dest.pedestal_hint_text else "unknown item"
|
||||
hint = dest.pedestal_hint_text
|
||||
else:
|
||||
hint = dest.hint_text if dest.hint_text else "something"
|
||||
hint = dest.hint_text
|
||||
if dest.player != player:
|
||||
if ped_hint:
|
||||
hint += f" for {world.player_name[dest.player]}!"
|
||||
@@ -2247,9 +2249,9 @@ def write_strings(rom, world, player):
|
||||
|
||||
# Lastly we write hints to show where certain interesting items are. It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless of how many exist. This supports many settings well.
|
||||
items_to_hint = RelevantItems.copy()
|
||||
if world.keyshuffle[player]:
|
||||
if world.smallkey_shuffle[player]:
|
||||
items_to_hint.extend(SmallKeys)
|
||||
if world.bigkeyshuffle[player]:
|
||||
if world.bigkey_shuffle[player]:
|
||||
items_to_hint.extend(BigKeys)
|
||||
local_random.shuffle(items_to_hint)
|
||||
hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'] else 8
|
||||
|
||||
@@ -8,6 +8,7 @@ from worlds.alttp.UnderworldGlitchRules import underworld_glitches_rules
|
||||
from worlds.alttp.Bosses import GanonDefeatRule
|
||||
from worlds.generic.Rules import set_rule, add_rule, forbid_item, add_item_rule, item_in_locations, \
|
||||
item_name
|
||||
from worlds.alttp.Options import smallkey_shuffle
|
||||
|
||||
|
||||
def set_rules(world):
|
||||
@@ -99,7 +100,7 @@ def set_rules(world):
|
||||
add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.world.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or')
|
||||
|
||||
set_bunny_rules(world, player, world.mode[player] == 'inverted')
|
||||
|
||||
|
||||
|
||||
def mirrorless_path_to_castle_courtyard(world, player):
|
||||
# If Agahnim is defeated then the courtyard needs to be accessible without using the mirror for the mirror offset glitch.
|
||||
@@ -211,17 +212,17 @@ def global_rules(world, player):
|
||||
set_rule(world.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player))
|
||||
|
||||
set_rule(world.get_entrance('Sewers Door', player),
|
||||
lambda state: state.has_key('Small Key (Hyrule Castle)', player) or (
|
||||
world.keyshuffle[player] == "universal" and world.mode[
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player) or (
|
||||
world.smallkey_shuffle[player] == smallkey_shuffle.option_universal and world.mode[
|
||||
player] == 'standard')) # standard universal small keys cannot access the shop
|
||||
set_rule(world.get_entrance('Sewers Back Door', player),
|
||||
lambda state: state.has_key('Small Key (Hyrule Castle)', player))
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
|
||||
set_rule(world.get_entrance('Agahnim 1', player),
|
||||
lambda state: state.has_sword(player) and state.has_key('Small Key (Agahnims Tower)', player, 2))
|
||||
lambda state: state.has_sword(player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
|
||||
|
||||
set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: state.can_kill_most_things(player, 8))
|
||||
set_rule(world.get_location('Castle Tower - Dark Maze', player),
|
||||
lambda state: state.can_kill_most_things(player, 8) and state.has_key('Small Key (Agahnims Tower)',
|
||||
lambda state: state.can_kill_most_things(player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)',
|
||||
player))
|
||||
|
||||
set_rule(world.get_location('Eastern Palace - Big Chest', player),
|
||||
@@ -238,15 +239,15 @@ def global_rules(world, player):
|
||||
|
||||
set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player))
|
||||
set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has('Pegasus Boots', player))
|
||||
set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state.has_key('Small Key (Desert Palace)', player))
|
||||
set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.world.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state.has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.world.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player))
|
||||
set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.world.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.world.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
|
||||
# logic patch to prevent placing a crystal in Desert that's required to reach the required keys
|
||||
if not (world.keyshuffle[player] and world.bigkeyshuffle[player]):
|
||||
if not (world.smallkey_shuffle[player] and world.bigkey_shuffle[player]):
|
||||
add_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.world.get_region('Desert Palace Main (Outer)', player).can_reach(state))
|
||||
|
||||
set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state.has_key('Small Key (Tower of Hera)', player) or item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player))
|
||||
set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Tower of Hera)', player) or item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player))
|
||||
set_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: state.has('Big Key (Tower of Hera)', player))
|
||||
set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
|
||||
set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: state.has_fire_source(player))
|
||||
@@ -254,36 +255,36 @@ def global_rules(world, player):
|
||||
set_always_allow(world.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
|
||||
|
||||
set_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
|
||||
set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state.has_key('Small Key (Swamp Palace)', player))
|
||||
set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player))
|
||||
set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player))
|
||||
set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player) or item_name(state, 'Swamp Palace - Big Chest', player) == ('Big Key (Swamp Palace)', player))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Swamp Palace - Big Chest', player), lambda state, item: item.name == 'Big Key (Swamp Palace)' and item.player == player)
|
||||
set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player))
|
||||
if not world.keyshuffle[player] and world.logic[player] != 'nologic':
|
||||
if not world.smallkey_shuffle[player] and world.logic[player] != 'nologic':
|
||||
forbid_item(world.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
|
||||
|
||||
set_rule(world.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
|
||||
set_rule(world.get_entrance('Blind Fight', player), lambda state: state.has_key('Small Key (Thieves Town)', player))
|
||||
set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state.has_key('Small Key (Thieves Town)', player) or item_name(state, 'Thieves\' Town - Big Chest', player) == ('Small Key (Thieves Town)', player)) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
||||
set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player) or item_name(state, 'Thieves\' Town - Big Chest', player) == ('Small Key (Thieves Town)', player)) and state.has('Hammer', player))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player and state.has('Hammer', player))
|
||||
set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state.has_key('Small Key (Thieves Town)', player))
|
||||
set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
||||
|
||||
set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state.has_key('Small Key (Skull Woods)', player))
|
||||
set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state.has_key('Small Key (Skull Woods)', player))
|
||||
set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 2)) # ideally would only be one key, but we may have spent thst key already on escaping the right section
|
||||
set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 2))
|
||||
set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player))
|
||||
set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player))
|
||||
set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2)) # ideally would only be one key, but we may have spent thst key already on escaping the right section
|
||||
set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2))
|
||||
set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) or item_name(state, 'Skull Woods - Big Chest', player) == ('Big Key (Skull Woods)', player))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Skull Woods - Big Chest', player), lambda state, item: item.name == 'Big Key (Skull Woods)' and item.player == player)
|
||||
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and state.has_sword(player)) # sword required for curtain
|
||||
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and state.has_sword(player)) # sword required for curtain
|
||||
|
||||
set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.can_melt_things(player))
|
||||
set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player))
|
||||
set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state.has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state.has_key('Small Key (Ice Palace)', player, 1))))
|
||||
set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1))))
|
||||
set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or (
|
||||
item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state.has_key('Small Key (Ice Palace)', player))) and (state.world.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)))
|
||||
item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.world.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)))
|
||||
set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player))
|
||||
|
||||
set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (state.has_sword(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player))) # need to defeat wizzrobes, bombs don't work ...
|
||||
@@ -292,13 +293,13 @@ def global_rules(world, player):
|
||||
set_rule(world.get_entrance('Misery Mire Big Key Door', player), lambda state: state.has('Big Key (Misery Mire)', player))
|
||||
# you can squander the free small key from the pot by opening the south door to the north west switch room, locking you out of accessing a color switch ...
|
||||
# big key gives backdoor access to that from the teleporter in the north west
|
||||
set_rule(world.get_location('Misery Mire - Map Chest', player), lambda state: state.has_key('Small Key (Misery Mire)', player, 1) or state.has('Big Key (Misery Mire)', player))
|
||||
set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state.has_key('Small Key (Misery Mire)', player, 1) or state.has_key('Big Key (Misery Mire)', player))
|
||||
set_rule(world.get_location('Misery Mire - Map Chest', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state.has('Big Key (Misery Mire)', player))
|
||||
set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state._lttp_has_key('Big Key (Misery Mire)', player))
|
||||
# we can place a small key in the West wing iff it also contains/blocks the Big Key, as we cannot reach and softlock with the basement key door yet
|
||||
set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state.has_key('Small Key (Misery Mire)', player, 2) if ((
|
||||
set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2) if ((
|
||||
item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or
|
||||
(
|
||||
item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state.has_key('Small Key (Misery Mire)', player, 3))
|
||||
item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state._lttp_has_key('Small Key (Misery Mire)', player, 3))
|
||||
set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: state.has_fire_source(player))
|
||||
set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: state.has_fire_source(player))
|
||||
set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player))
|
||||
@@ -317,27 +318,27 @@ def global_rules(world, player):
|
||||
set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 4) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
|
||||
set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
|
||||
|
||||
if not world.enemy_shuffle[player]:
|
||||
set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: state.can_shoot_arrows(player))
|
||||
set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop', player), lambda state: state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area
|
||||
set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and state.can_shoot_arrows(player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Palace of Darkness (North)', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 4))
|
||||
set_rule(world.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area
|
||||
set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and state.can_shoot_arrows(player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))
|
||||
set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player))
|
||||
|
||||
set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||
item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state.has_key('Small Key (Palace of Darkness)', player, 3)))
|
||||
set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||
item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3)))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state.has_key('Small Key (Palace of Darkness)', player, 5))
|
||||
set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
||||
|
||||
set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||
item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state.has_key('Small Key (Palace of Darkness)', player, 4)))
|
||||
set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||
item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state.has_key('Small Key (Palace of Darkness)', player, 5))
|
||||
set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
||||
|
||||
set_rule(world.get_entrance('Palace of Darkness Maze Door', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6))
|
||||
set_rule(world.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6))
|
||||
|
||||
# these key rules are conservative, you might be able to get away with more lenient rules
|
||||
randomizer_room_chests = ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right']
|
||||
@@ -346,30 +347,30 @@ def global_rules(world, player):
|
||||
set_rule(world.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player))
|
||||
set_rule(world.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
|
||||
set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 4) or (
|
||||
item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state.has_key('Small Key (Ganons Tower)', player, 3)))
|
||||
set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
|
||||
item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state.has_key('Small Key (Ganons Tower)', player, 3) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
|
||||
set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
|
||||
|
||||
# It is possible to need more than 2 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.
|
||||
# However we need to leave these at the lower values to derive that with 3 keys it is always possible to reach Bob and Ice Armos.
|
||||
set_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 2))
|
||||
set_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 2))
|
||||
# It is possible to need more than 3 keys ....
|
||||
set_rule(world.get_entrance('Ganons Tower (Firesnake Room)', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 3))
|
||||
set_rule(world.get_entrance('Ganons Tower (Firesnake Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3))
|
||||
|
||||
#The actual requirements for these rooms to avoid key-lock
|
||||
set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 3) or ((
|
||||
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_in_locations(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state.has_key('Small Key (Ganons Tower)', player, 2)))
|
||||
set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) or ((
|
||||
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_in_locations(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 2)))
|
||||
for location in randomizer_room_chests:
|
||||
set_rule(world.get_location(location, player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 4) or (
|
||||
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state.has_key('Small Key (Ganons Tower)', player, 3)))
|
||||
set_rule(world.get_location(location, player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
|
||||
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))
|
||||
|
||||
# Once again it is possible to need more than 3 keys...
|
||||
set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 3) and state.has('Fire Rod', player))
|
||||
set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.has('Fire Rod', player))
|
||||
# Actual requirements
|
||||
for location in compass_room_chests:
|
||||
set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state.has_key('Small Key (Ganons Tower)', player, 4) or (
|
||||
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state.has_key('Small Key (Ganons Tower)', player, 3))))
|
||||
set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
|
||||
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))))
|
||||
|
||||
set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player))
|
||||
|
||||
@@ -388,9 +389,9 @@ def global_rules(world, player):
|
||||
set_rule(world.get_entrance('Ganons Tower Torch Rooms', player),
|
||||
lambda state: state.has_fire_source(player) and state.world.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
|
||||
set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest', player),
|
||||
lambda state: state.has_key('Small Key (Ganons Tower)', player, 3))
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3))
|
||||
set_rule(world.get_entrance('Ganons Tower Moldorm Door', player),
|
||||
lambda state: state.has_key('Small Key (Ganons Tower)', player, 4))
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4))
|
||||
set_rule(world.get_entrance('Ganons Tower Moldorm Gap', player),
|
||||
lambda state: state.has('Hookshot', player) and state.world.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state))
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player))
|
||||
@@ -799,14 +800,14 @@ def add_conditional_lamps(world, player):
|
||||
def open_rules(world, player):
|
||||
# softlock protection as you can reach the sewers small key door with a guard drop key
|
||||
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player),
|
||||
lambda state: state.has_key('Small Key (Hyrule Castle)', player))
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
|
||||
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
|
||||
lambda state: state.has_key('Small Key (Hyrule Castle)', player))
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
|
||||
|
||||
|
||||
def swordless_rules(world, player):
|
||||
set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or state.can_shoot_arrows(player) or state.has('Cane of Somaria', player)) and state.has_key('Small Key (Agahnims Tower)', player, 2))
|
||||
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain
|
||||
set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or state.can_shoot_arrows(player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
|
||||
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain
|
||||
set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) #in swordless mode bombos pads are present in the relevant parts of ice palace
|
||||
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop
|
||||
|
||||
@@ -849,10 +850,12 @@ def toss_junk_item(world, player):
|
||||
|
||||
def set_trock_key_rules(world, player):
|
||||
# First set all relevant locked doors to impassible.
|
||||
for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Pokey Room']:
|
||||
for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Pokey Room', 'Turtle Rock Big Key Door']:
|
||||
set_rule(world.get_entrance(entrance, player), lambda state: False)
|
||||
|
||||
all_state = world.get_all_state(True)
|
||||
all_state = world.get_all_state(use_cache=False)
|
||||
all_state.reachable_regions[player] = set() # wipe reachable regions so that the locked doors actually work
|
||||
all_state.stale[player] = True
|
||||
|
||||
# Check if each of the four main regions of the dungoen can be reached. The previous code section prevents key-costing moves within the dungeon.
|
||||
can_reach_back = all_state.can_reach(world.get_region('Turtle Rock (Eye Bridge)', player)) if world.can_access_trock_eyebridge[player] is None else world.can_access_trock_eyebridge[player]
|
||||
@@ -877,28 +880,32 @@ def set_trock_key_rules(world, player):
|
||||
|
||||
# The following represent the common key rules.
|
||||
|
||||
# Big key door requires the big key, obviously. We removed this rule in the previous section to flag front_locked_locations correctly,
|
||||
# otherwise crystaroller room might not be properly marked as reachable through the back.
|
||||
set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player))
|
||||
|
||||
# No matter what, the key requirement for going from the middle to the bottom should be three keys.
|
||||
set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 3))
|
||||
set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
|
||||
|
||||
# Now we need to set rules based on which entrances we have access to. The most important point is whether we have back access. If we have back access, we
|
||||
# might open all the locked doors in any order so we need maximally restrictive rules.
|
||||
if can_reach_back:
|
||||
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state.has_key('Small Key (Turtle Rock)', player, 4) or item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 4))
|
||||
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 4) or item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4))
|
||||
# Only consider wasting the key on the Trinexx door for going from the front entrance to middle section. If other key doors are accessible, then these doors can be avoided
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 3))
|
||||
set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 2))
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
|
||||
set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
|
||||
else:
|
||||
# Middle to front requires 2 keys if the back is locked, otherwise 4
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 2)
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2)
|
||||
if item_in_locations(state, 'Big Key (Turtle Rock)', player, front_locked_locations)
|
||||
else state.has_key('Small Key (Turtle Rock)', player, 4))
|
||||
else state._lttp_has_key('Small Key (Turtle Rock)', player, 4))
|
||||
|
||||
# Front to middle requires 2 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted)
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 2))
|
||||
set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 1))
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
|
||||
set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 1))
|
||||
|
||||
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state)))
|
||||
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state)))
|
||||
|
||||
def tr_big_key_chest_keys_needed(state):
|
||||
# This function handles the key requirements for the TR Big Chest in the situations it having the Big Key should logically require 2 keys, small key
|
||||
@@ -911,14 +918,14 @@ def set_trock_key_rules(world, player):
|
||||
return 4
|
||||
|
||||
# If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential
|
||||
if not can_reach_front and not world.keyshuffle[player]:
|
||||
if not can_reach_front and not world.smallkey_shuffle[player]:
|
||||
# Must not go in the Big Key Chest - only 1 other chest available and 2+ keys required for all other chests
|
||||
forbid_item(world.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player)
|
||||
if not can_reach_big_chest:
|
||||
# Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests
|
||||
forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
|
||||
if world.accessibility[player] == 'locations' and world.goal[player] != 'icerodhunt':
|
||||
if world.bigkeyshuffle[player] and can_reach_big_chest:
|
||||
if world.bigkey_shuffle[player] and can_reach_big_chest:
|
||||
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
|
||||
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
|
||||
'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right']:
|
||||
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
from worlds.alttp.SubClasses import ALttPLocation
|
||||
from worlds.alttp.EntranceShuffle import door_addresses
|
||||
from worlds.alttp.Items import item_name_groups, item_table, ItemFactory, trap_replaceable, GetBeemizerItem
|
||||
from worlds.alttp.Options import smallkey_shuffle
|
||||
from Utils import int16_as_bytes
|
||||
|
||||
logger = logging.getLogger("Shops")
|
||||
@@ -22,6 +23,11 @@ class Shop():
|
||||
slots: int = 3 # slot count is not dynamic in asm, however inventory can have None as empty slots
|
||||
blacklist: Set[str] = set() # items that don't work, todo: actually check against this
|
||||
type = ShopType.Shop
|
||||
slot_names: Dict[int, str] = {
|
||||
0: "Left",
|
||||
1: "Center",
|
||||
2: "Right"
|
||||
}
|
||||
|
||||
def __init__(self, region, room_id: int, shopkeeper_config: int, custom: bool, locked: bool, sram_offset: int):
|
||||
self.region = region
|
||||
@@ -131,23 +137,22 @@ shop_class_mapping = {ShopType.UpgradeShop: UpgradeShop,
|
||||
|
||||
def FillDisabledShopSlots(world):
|
||||
shop_slots: Set[ALttPLocation] = {location for shop_locations in (shop.region.locations for shop in world.shops)
|
||||
for location in shop_locations if location.shop_slot and location.shop_slot_disabled}
|
||||
for location in shop_locations
|
||||
if location.shop_slot is not None and location.shop_slot_disabled}
|
||||
for location in shop_slots:
|
||||
location.shop_slot_disabled = True
|
||||
slot_num = int(location.name[-1]) - 1
|
||||
shop: Shop = location.parent_region.shop
|
||||
location.item = ItemFactory(shop.inventory[slot_num]['item'], location.player)
|
||||
location.item = ItemFactory(shop.inventory[location.shop_slot]['item'], location.player)
|
||||
location.item_rule = lambda item: item.name == location.item.name and item.player == location.player
|
||||
|
||||
|
||||
def ShopSlotFill(world):
|
||||
shop_slots: Set[ALttPLocation] = {location for shop_locations in (shop.region.locations for shop in world.shops)
|
||||
for location in shop_locations if location.shop_slot}
|
||||
for location in shop_locations if location.shop_slot is not None}
|
||||
removed = set()
|
||||
for location in shop_slots:
|
||||
slot_num = int(location.name[-1]) - 1
|
||||
shop: Shop = location.parent_region.shop
|
||||
if not shop.can_push_inventory(slot_num) or location.shop_slot_disabled:
|
||||
if not shop.can_push_inventory(location.shop_slot) or location.shop_slot_disabled:
|
||||
location.shop_slot_disabled = True
|
||||
removed.add(location)
|
||||
|
||||
@@ -155,6 +160,7 @@ def ShopSlotFill(world):
|
||||
shop_slots -= removed
|
||||
|
||||
if shop_slots:
|
||||
logger.info("Filling LttP Shop Slots")
|
||||
del shop_slots
|
||||
|
||||
from Fill import swap_location_item
|
||||
@@ -179,7 +185,7 @@ def ShopSlotFill(world):
|
||||
shops_per_sphere.append(current_shops_slots)
|
||||
candidates_per_sphere.append(current_candidates)
|
||||
for location in sphere:
|
||||
if location.shop_slot:
|
||||
if location.shop_slot is not None:
|
||||
if not location.shop_slot_disabled:
|
||||
current_shops_slots.append(location)
|
||||
elif not location.locked and not location.item.name in blacklist_words:
|
||||
@@ -229,7 +235,7 @@ def ShopSlotFill(world):
|
||||
else:
|
||||
price = world.random.randrange(8, 56)
|
||||
|
||||
shop.push_inventory(int(location.name[-1]) - 1, item_name, price * 5, 1,
|
||||
shop.push_inventory(location.shop_slot, item_name, price * 5, 1,
|
||||
location.item.player if location.item.player != location.player else 0)
|
||||
|
||||
|
||||
@@ -266,7 +272,7 @@ def create_shops(world, player: int):
|
||||
# make sure that blue potion is available in inverted, special case locked = None; lock when done.
|
||||
player_shop_table["Dark Lake Hylia Shop"] = \
|
||||
player_shop_table["Dark Lake Hylia Shop"]._replace(items=_inverted_hylia_shop_defaults, locked=None)
|
||||
chance_100 = int(world.retro[player])*0.25+int(world.keyshuffle[player] == "universal") * 0.5
|
||||
chance_100 = int(world.retro[player])*0.25+int(world.smallkey_shuffle[player] == smallkey_shuffle.option_universal) * 0.5
|
||||
for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram_offset) in player_shop_table.items():
|
||||
region = world.get_region(region_name, player)
|
||||
shop: Shop = shop_class_mapping[type](region, room_id, shopkeeper, custom, locked, sram_offset)
|
||||
@@ -278,10 +284,10 @@ def create_shops(world, player: int):
|
||||
for index, item in enumerate(inventory):
|
||||
shop.add_inventory(index, *item)
|
||||
if not locked and num_slots:
|
||||
slot_name = "{} Slot {}".format(region.name, index + 1)
|
||||
slot_name = f"{region.name} {shop.slot_names[index]}"
|
||||
loc = ALttPLocation(player, slot_name, address=shop_table_by_location[slot_name],
|
||||
parent=region, hint_text="for sale")
|
||||
loc.shop_slot = True
|
||||
loc.shop_slot = index
|
||||
loc.locked = True
|
||||
if single_purchase_slots.pop():
|
||||
if world.goal[player] != 'icerodhunt':
|
||||
@@ -337,9 +343,10 @@ total_shop_slots = len(shop_table) * 3
|
||||
total_dynamic_shop_slots = sum(3 for shopname, data in shop_table.items() if not data[4]) # data[4] -> locked
|
||||
|
||||
SHOP_ID_START = 0x400000
|
||||
shop_table_by_location_id = {cnt: s for cnt, s in enumerate(
|
||||
(f"{name} Slot {num}" for name in [key for key, value in sorted(shop_table.items(), key=lambda item: item[1].sram_offset)]
|
||||
for num in range(1, 4)), start=SHOP_ID_START)}
|
||||
shop_table_by_location_id = dict(enumerate(
|
||||
(f"{name} {Shop.slot_names[num]}" for name, shop_data in sorted(shop_table.items(), key=lambda item: item[1].sram_offset)
|
||||
for num in range(3)), start=SHOP_ID_START))
|
||||
|
||||
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots)] = "Old Man Sword Cave"
|
||||
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots + 1)] = "Take-Any #1"
|
||||
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots + 2)] = "Take-Any #2"
|
||||
@@ -365,13 +372,13 @@ def set_up_shops(world, player: int):
|
||||
rss = world.get_region('Red Shield Shop', player).shop
|
||||
replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50],
|
||||
['Blue Shield', 50], ['Small Heart', 10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them.
|
||||
if world.keyshuffle[player] == "universal":
|
||||
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||
replacement_items.append(['Small Key (Universal)', 100])
|
||||
replacement_item = world.random.choice(replacement_items)
|
||||
rss.add_inventory(2, 'Single Arrow', 80, 1, replacement_item[0], replacement_item[1])
|
||||
rss.locked = True
|
||||
|
||||
if world.keyshuffle[player] == "universal" or world.retro[player]:
|
||||
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal or world.retro[player]:
|
||||
for shop in world.random.sample([s for s in world.shops if
|
||||
s.custom and not s.locked and s.type == ShopType.Shop and s.region.player == player],
|
||||
5):
|
||||
@@ -379,7 +386,7 @@ def set_up_shops(world, player: int):
|
||||
slots = [0, 1, 2]
|
||||
world.random.shuffle(slots)
|
||||
slots = iter(slots)
|
||||
if world.keyshuffle[player] == "universal":
|
||||
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||
shop.add_inventory(next(slots), 'Small Key (Universal)', 100)
|
||||
if world.retro[player]:
|
||||
shop.push_inventory(next(slots), 'Single Arrow', 80)
|
||||
|
||||
@@ -18,6 +18,7 @@ class ALttPLocation(Location):
|
||||
|
||||
class ALttPItem(Item):
|
||||
game: str = "A Link to the Past"
|
||||
dungeon = None
|
||||
|
||||
def __init__(self, name, player, advancement=False, type=None, item_code=None, pedestal_hint=None, pedestal_credit=None,
|
||||
sick_kid_credit=None, zora_credit=None, witch_credit=None, flute_boy_credit=None, hint_text=None):
|
||||
@@ -29,4 +30,33 @@ class ALttPItem(Item):
|
||||
self.zora_credit_text = zora_credit
|
||||
self.magicshop_credit_text = witch_credit
|
||||
self.fluteboy_credit_text = flute_boy_credit
|
||||
self._hint_text = hint_text
|
||||
self._hint_text = hint_text
|
||||
|
||||
@property
|
||||
def crystal(self) -> bool:
|
||||
return self.type == 'Crystal'
|
||||
|
||||
@property
|
||||
def smallkey(self) -> bool:
|
||||
return self.type == 'SmallKey'
|
||||
|
||||
@property
|
||||
def bigkey(self) -> bool:
|
||||
return self.type == 'BigKey'
|
||||
|
||||
@property
|
||||
def map(self) -> bool:
|
||||
return self.type == 'Map'
|
||||
|
||||
@property
|
||||
def compass(self) -> bool:
|
||||
return self.type == 'Compass'
|
||||
|
||||
@property
|
||||
def dungeon_item(self) -> Optional[str]:
|
||||
if self.type in {"SmallKey", "BigKey", "Map", "Compass"}:
|
||||
return self.type
|
||||
|
||||
@property
|
||||
def locked_dungeon_item(self):
|
||||
return self.location.locked and self.dungeon_item
|
||||
@@ -2,16 +2,17 @@ import random
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import typing
|
||||
|
||||
from BaseClasses import Item, CollectionState
|
||||
from .SubClasses import ALttPItem
|
||||
from ..AutoWorld import World
|
||||
from .Options import alttp_options
|
||||
from ..AutoWorld import World, LogicMixin
|
||||
from .Options import alttp_options, smallkey_shuffle
|
||||
from .Items import as_dict_item_table, item_name_groups, item_table
|
||||
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions
|
||||
from .Rules import set_rules
|
||||
from .ItemPool import generate_itempool
|
||||
from .Shops import create_shops
|
||||
from .ItemPool import generate_itempool, difficulties
|
||||
from .Shops import create_shops, ShopSlotFill
|
||||
from .Dungeons import create_dungeons
|
||||
from .Rom import LocalRom, patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, get_hash_string
|
||||
import Patch
|
||||
@@ -21,28 +22,69 @@ from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_con
|
||||
|
||||
lttp_logger = logging.getLogger("A Link to the Past")
|
||||
|
||||
|
||||
class ALTTPWorld(World):
|
||||
"""
|
||||
The Legend of Zelda: A Link to the Past is an action/adventure game. Take on the role of
|
||||
Link, a boy who is destined to save the land of Hyrule. Delve through three palaces and nine
|
||||
dungeons on your quest to rescue the descendents of the seven wise men and defeat the evil
|
||||
Ganon!
|
||||
"""
|
||||
game: str = "A Link to the Past"
|
||||
options = alttp_options
|
||||
topology_present = True
|
||||
item_name_groups = item_name_groups
|
||||
item_names = frozenset(item_table)
|
||||
location_names = frozenset(lookup_name_to_id)
|
||||
hint_blacklist = {"Triforce"}
|
||||
|
||||
item_name_to_id = {name: data.item_code for name, data in item_table.items() if type(data.item_code) == int}
|
||||
location_name_to_id = lookup_name_to_id
|
||||
|
||||
data_version = 7
|
||||
data_version = 8
|
||||
remote_items: bool = False
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
create_items = generate_itempool
|
||||
|
||||
def create_regions(self):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.dungeon_local_item_names = set()
|
||||
self.dungeon_specific_item_names = set()
|
||||
self.rom_name_available_event = threading.Event()
|
||||
super(ALTTPWorld, self).__init__(*args, **kwargs)
|
||||
|
||||
def generate_early(self):
|
||||
player = self.player
|
||||
world = self.world
|
||||
|
||||
# system for sharing ER layouts
|
||||
world.er_seeds[player] = str(world.random.randint(0, 2 ** 64))
|
||||
|
||||
if "-" in world.shuffle[player]:
|
||||
shuffle, seed = world.shuffle[player].split("-", 1)
|
||||
world.shuffle[player] = shuffle
|
||||
if shuffle == "vanilla":
|
||||
world.er_seeds[player] = "vanilla"
|
||||
elif seed.startswith("group-") or world.is_race:
|
||||
world.er_seeds[player] = get_same_seed(world, (
|
||||
shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
|
||||
else: # not a race or group seed, use set seed as is.
|
||||
world.er_seeds[player] = seed
|
||||
elif world.shuffle[player] == "vanilla":
|
||||
world.er_seeds[player] = "vanilla"
|
||||
for dungeon_item in ["smallkey_shuffle", "bigkey_shuffle", "compass_shuffle", "map_shuffle"]:
|
||||
option = getattr(world, dungeon_item)[player]
|
||||
if option == "own_world":
|
||||
world.local_items[player] |= self.item_name_groups[option.item_name_group]
|
||||
elif option == "different_world":
|
||||
world.non_local_items[player] |= self.item_name_groups[option.item_name_group]
|
||||
elif option.in_dungeon:
|
||||
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
|
||||
if option == "original_dungeon":
|
||||
self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group]
|
||||
|
||||
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
||||
|
||||
def create_regions(self):
|
||||
player = self.player
|
||||
world = self.world
|
||||
if world.open_pyramid[player] == 'goal':
|
||||
@@ -67,7 +109,6 @@ class ALTTPWorld(World):
|
||||
create_shops(world, player)
|
||||
create_dungeons(world, player)
|
||||
|
||||
|
||||
if world.logic[player] not in ["noglitches", "minorglitches"] and world.shuffle[player] in \
|
||||
{"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}:
|
||||
world.fix_fake_world[player] = False
|
||||
@@ -86,53 +127,92 @@ class ALTTPWorld(World):
|
||||
world.random = old_random
|
||||
plando_connect(world, player)
|
||||
|
||||
def collect_item(self, state: CollectionState, item: Item):
|
||||
if item.name.startswith('Progressive '):
|
||||
if 'Sword' in item.name:
|
||||
if state.has('Golden Sword', item.player):
|
||||
pass
|
||||
elif state.has('Tempered Sword', item.player) and self.world.difficulty_requirements[
|
||||
item.player].progressive_sword_limit >= 4:
|
||||
return 'Golden Sword'
|
||||
elif state.has('Master Sword', item.player) and self.world.difficulty_requirements[
|
||||
item.player].progressive_sword_limit >= 3:
|
||||
return 'Tempered Sword'
|
||||
elif state.has('Fighter Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 2:
|
||||
return 'Master Sword'
|
||||
elif self.world.difficulty_requirements[item.player].progressive_sword_limit >= 1:
|
||||
return 'Fighter Sword'
|
||||
elif 'Glove' in item.name:
|
||||
if state.has('Titans Mitts', item.player):
|
||||
return
|
||||
elif state.has('Power Glove', item.player):
|
||||
return 'Titans Mitts'
|
||||
else:
|
||||
return 'Power Glove'
|
||||
elif 'Shield' in item.name:
|
||||
if state.has('Mirror Shield', item.player):
|
||||
return
|
||||
elif state.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3:
|
||||
return 'Mirror Shield'
|
||||
elif state.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2:
|
||||
return 'Red Shield'
|
||||
elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1:
|
||||
return 'Blue Shield'
|
||||
elif 'Bow' in item.name:
|
||||
if state.has('Silver', item.player):
|
||||
return
|
||||
elif state.has('Bow', item.player) and self.world.difficulty_requirements[item.player].progressive_bow_limit >= 2:
|
||||
return 'Silver Bow'
|
||||
elif self.world.difficulty_requirements[item.player].progressive_bow_limit >= 1:
|
||||
return 'Bow'
|
||||
def collect_item(self, state: CollectionState, item: Item, remove=False):
|
||||
item_name = item.name
|
||||
if item_name.startswith('Progressive '):
|
||||
if remove:
|
||||
if 'Sword' in item_name:
|
||||
if state.has('Golden Sword', item.player):
|
||||
return 'Golden Sword'
|
||||
elif state.has('Tempered Sword', item.player):
|
||||
return 'Tempered Sword'
|
||||
elif state.has('Master Sword', item.player):
|
||||
return 'Master Sword'
|
||||
elif state.has('Fighter Sword', item.player):
|
||||
return 'Fighter Sword'
|
||||
else:
|
||||
return None
|
||||
elif 'Glove' in item.name:
|
||||
if state.has('Titans Mitts', item.player):
|
||||
return 'Titans Mitts'
|
||||
elif state.has('Power Glove', item.player):
|
||||
return 'Power Glove'
|
||||
else:
|
||||
return None
|
||||
elif 'Shield' in item_name:
|
||||
if state.has('Mirror Shield', item.player):
|
||||
return 'Mirror Shield'
|
||||
elif state.has('Red Shield', item.player):
|
||||
return 'Red Shield'
|
||||
elif state.has('Blue Shield', item.player):
|
||||
return 'Blue Shield'
|
||||
else:
|
||||
return None
|
||||
elif 'Bow' in item_name:
|
||||
if state.has('Silver Bow', item.player):
|
||||
return 'Silver Bow'
|
||||
elif state.has('Bow', item.player):
|
||||
return 'Bow'
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
if 'Sword' in item_name:
|
||||
if state.has('Golden Sword', item.player):
|
||||
pass
|
||||
elif state.has('Tempered Sword', item.player) and self.world.difficulty_requirements[
|
||||
item.player].progressive_sword_limit >= 4:
|
||||
return 'Golden Sword'
|
||||
elif state.has('Master Sword', item.player) and self.world.difficulty_requirements[
|
||||
item.player].progressive_sword_limit >= 3:
|
||||
return 'Tempered Sword'
|
||||
elif state.has('Fighter Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 2:
|
||||
return 'Master Sword'
|
||||
elif self.world.difficulty_requirements[item.player].progressive_sword_limit >= 1:
|
||||
return 'Fighter Sword'
|
||||
elif 'Glove' in item_name:
|
||||
if state.has('Titans Mitts', item.player):
|
||||
return
|
||||
elif state.has('Power Glove', item.player):
|
||||
return 'Titans Mitts'
|
||||
else:
|
||||
return 'Power Glove'
|
||||
elif 'Shield' in item_name:
|
||||
if state.has('Mirror Shield', item.player):
|
||||
return
|
||||
elif state.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3:
|
||||
return 'Mirror Shield'
|
||||
elif state.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2:
|
||||
return 'Red Shield'
|
||||
elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1:
|
||||
return 'Blue Shield'
|
||||
elif 'Bow' in item_name:
|
||||
if state.has('Silver Bow', item.player):
|
||||
return
|
||||
elif state.has('Bow', item.player) and (self.world.difficulty_requirements[item.player].progressive_bow_limit >= 2
|
||||
or self.world.logic[item.player] == 'noglitches'
|
||||
or self.world.swordless[item.player]): # modes where silver bow is always required for ganon
|
||||
return 'Silver Bow'
|
||||
elif self.world.difficulty_requirements[item.player].progressive_bow_limit >= 1:
|
||||
return 'Bow'
|
||||
elif item.advancement:
|
||||
return item.name
|
||||
return item_name
|
||||
|
||||
def pre_fill(self):
|
||||
from Fill import fill_restrictive, FillError
|
||||
attempts = 5
|
||||
world = self.world
|
||||
player = self.player
|
||||
all_state = world.get_all_state(keys=True)
|
||||
all_state = world.get_all_state(use_cache=True)
|
||||
crystals = [self.create_item(name) for name in ['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6']]
|
||||
crystal_locations = [world.get_location('Turtle Rock - Prize', player),
|
||||
world.get_location('Eastern Palace - Prize', player),
|
||||
@@ -166,7 +246,11 @@ class ALTTPWorld(World):
|
||||
@classmethod
|
||||
def stage_pre_fill(cls, world):
|
||||
from .Dungeons import fill_dungeons_restrictive
|
||||
fill_dungeons_restrictive(world)
|
||||
fill_dungeons_restrictive(cls, world)
|
||||
|
||||
@classmethod
|
||||
def stage_post_fill(cls, world):
|
||||
ShopSlotFill(world)
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
world = self.world
|
||||
@@ -242,11 +326,13 @@ class ALTTPWorld(World):
|
||||
return ALttPItem(name, self.player, **as_dict_item_table[name])
|
||||
|
||||
@classmethod
|
||||
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, restitempool, fill_locations):
|
||||
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
|
||||
restitempool, fill_locations):
|
||||
trash_counts = {}
|
||||
standard_keyshuffle_players = set()
|
||||
for player in world.get_game_players("A Link to the Past"):
|
||||
if world.mode[player] == 'standard' and world.keyshuffle[player] is True:
|
||||
if world.mode[player] == 'standard' and world.smallkey_shuffle[player] \
|
||||
and world.smallkey_shuffle[player] != smallkey_shuffle.option_universal:
|
||||
standard_keyshuffle_players.add(player)
|
||||
if not world.ganonstower_vanilla[player] or \
|
||||
world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}:
|
||||
@@ -304,3 +390,21 @@ class ALTTPWorld(World):
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
fill_locations.remove(spot_to_fill) # very slow, unfortunately
|
||||
trash_count -= 1
|
||||
|
||||
|
||||
def get_same_seed(world, seed_def: tuple) -> str:
|
||||
seeds: typing.Dict[tuple, str] = getattr(world, "__named_seeds", {})
|
||||
if seed_def in seeds:
|
||||
return seeds[seed_def]
|
||||
seeds[seed_def] = str(world.random.randint(0, 2 ** 64))
|
||||
world.__named_seeds = seeds
|
||||
return seeds[seed_def]
|
||||
|
||||
|
||||
class ALttPLogic(LogicMixin):
|
||||
def _lttp_has_key(self, item, player, count: int = 1):
|
||||
if self.world.logic[player] == 'nologic':
|
||||
return True
|
||||
if self.world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||
return self.can_buy_unlimited('Small Key (Universal)', player)
|
||||
return self.prog_items[item, player] >= count
|
||||
|
||||
@@ -24,6 +24,11 @@ all_items["Evolution Trap"] = factorio_base_id - 2
|
||||
|
||||
|
||||
class Factorio(World):
|
||||
"""
|
||||
Factorio is a game about automation. You play as an engineer who has crash landed on the planet
|
||||
Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory,
|
||||
research new technologies, and become more efficient in your quest to build a rocket and return home.
|
||||
"""
|
||||
game: str = "Factorio"
|
||||
static_nodes = {"automation", "logistics", "rocket-silo"}
|
||||
custom_recipes = {}
|
||||
@@ -140,14 +145,19 @@ class Factorio(World):
|
||||
|
||||
world.completion_condition[player] = lambda state: state.has('Victory', player)
|
||||
|
||||
def collect_item(self, state, item):
|
||||
def collect_item(self, state, item, remove=False):
|
||||
if item.advancement and item.name in progressive_technology_table:
|
||||
prog_table = progressive_technology_table[item.name].progressive
|
||||
for item_name in prog_table:
|
||||
if not state.has(item_name, item.player):
|
||||
return item_name
|
||||
if remove:
|
||||
for item_name in reversed(prog_table):
|
||||
if state.has(item_name, item.player):
|
||||
return item_name
|
||||
else:
|
||||
for item_name in prog_table:
|
||||
if not state.has(item_name, item.player):
|
||||
return item_name
|
||||
|
||||
return super(Factorio, self).collect_item(state, item)
|
||||
return super(Factorio, self).collect_item(state, item, remove)
|
||||
|
||||
def get_required_client_version(self) -> tuple:
|
||||
return max((0, 1, 6), super(Factorio, self).get_required_client_version())
|
||||
@@ -179,10 +189,7 @@ class Factorio(World):
|
||||
max_energy = remaining_energy * 0.75
|
||||
min_energy = (remaining_energy - max_energy) / remaining_num_ingredients
|
||||
ingredient = pool.pop()
|
||||
if ingredient not in recipes:
|
||||
logging.warning(f"missing recipe for {ingredient}")
|
||||
continue
|
||||
ingredient_recipe = recipes[ingredient]
|
||||
ingredient_recipe = min(all_product_sources[ingredient], key=lambda recipe: recipe.rel_cost)
|
||||
ingredient_raw = sum((count for ingredient, count in ingredient_recipe.base_cost.items()))
|
||||
ingredient_energy = ingredient_recipe.total_energy
|
||||
min_num_raw = min_raw/ingredient_raw
|
||||
|
||||
@@ -13,6 +13,7 @@ def exclusion_rules(world, player: int, excluded_locations: set):
|
||||
for loc_name in excluded_locations:
|
||||
location = world.get_location(loc_name, player)
|
||||
add_item_rule(location, lambda i: not (i.advancement or i.never_exclude))
|
||||
location.excluded = True
|
||||
|
||||
|
||||
def set_rule(spot, rule):
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
from typing import NamedTuple, Union
|
||||
import logging
|
||||
|
||||
from ..AutoWorld import World
|
||||
|
||||
|
||||
class GenericWorld(World):
|
||||
game = "Archipelago"
|
||||
topology_present = False
|
||||
item_name_to_id = {
|
||||
"Nothing": -1
|
||||
}
|
||||
location_name_to_id = {
|
||||
"Cheat Console" : -1,
|
||||
"Server": -2
|
||||
}
|
||||
hidden = True
|
||||
|
||||
class PlandoItem(NamedTuple):
|
||||
item: str
|
||||
|
||||
@@ -19,6 +19,8 @@ class HKWorld(World):
|
||||
item_name_to_id = {name: data.id for name, data in item_table.items() if data.type != "Event"}
|
||||
location_name_to_id = lookup_name_to_id
|
||||
|
||||
hidden = True
|
||||
|
||||
def generate_basic(self):
|
||||
# Link regions
|
||||
self.world.get_entrance('Hollow Nest S&Q', self.player).connect(self.world.get_region('Hollow Nest', self.player))
|
||||
|
||||
@@ -58,6 +58,7 @@ item_table = {
|
||||
"Dragon Egg Shard": ItemData(45043, True),
|
||||
"Bee Trap (Minecraft)": ItemData(45100, False),
|
||||
|
||||
"Blaze Rods": ItemData(None, True),
|
||||
"Victory": ItemData(None, True)
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@ advancement_table = {
|
||||
"Librarian": AdvData(42090, 'Overworld'),
|
||||
"Overpowered": AdvData(42091, 'Overworld'),
|
||||
|
||||
"Blaze Spawner": AdvData(None, 'Nether Fortress'),
|
||||
"Ender Dragon": AdvData(None, 'The End')
|
||||
}
|
||||
|
||||
|
||||
@@ -3,37 +3,83 @@ from Options import Choice, Option, Toggle, Range
|
||||
|
||||
|
||||
class AdvancementGoal(Range):
|
||||
"""Number of advancements required to spawn the Ender Dragon."""
|
||||
displayname = "Advancement Goal"
|
||||
range_start = 0
|
||||
range_end = 87
|
||||
default = 50
|
||||
|
||||
|
||||
class EggShardsRequired(Range):
|
||||
"""Number of dragon egg shards to collect before the Ender Dragon will spawn."""
|
||||
displayname = "Egg Shards Required"
|
||||
range_start = 0
|
||||
range_end = 30
|
||||
|
||||
|
||||
class EggShardsAvailable(Range):
|
||||
"""Number of dragon egg shards available to collect."""
|
||||
displayname = "Egg Shards Available"
|
||||
range_start = 0
|
||||
range_end = 30
|
||||
|
||||
|
||||
class ShuffleStructures(Toggle):
|
||||
"""Enables shuffling of villages, outposts, fortresses, bastions, and end cities."""
|
||||
displayname = "Shuffle Structures"
|
||||
|
||||
|
||||
class StructureCompasses(Toggle):
|
||||
"""Adds structure compasses to the item pool, which point to the nearest indicated structure."""
|
||||
displayname = "Structure Compasses"
|
||||
|
||||
|
||||
class BeeTraps(Range):
|
||||
"""Replaces a percentage of junk items with bee traps, which spawn multiple angered bees around every player when received."""
|
||||
displayname = "Bee Trap Percentage"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
|
||||
|
||||
class CombatDifficulty(Choice):
|
||||
"""Modifies the level of items logically required for exploring dangerous areas and fighting bosses."""
|
||||
displayname = "Combat Difficulty"
|
||||
option_easy = 0
|
||||
option_normal = 1
|
||||
option_hard = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class BeeTraps(Range):
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
class HardAdvancements(Toggle):
|
||||
"""Enables certain RNG-reliant or tedious advancements."""
|
||||
displayname = "Include Hard Advancements"
|
||||
|
||||
|
||||
class EggShards(Range):
|
||||
range_start = 0
|
||||
range_end = 30
|
||||
class InsaneAdvancements(Toggle):
|
||||
"""Enables the extremely difficult advancements "How Did We Get Here?" and "Adventuring Time.\""""
|
||||
displayname = "Include Insane Advancements"
|
||||
|
||||
|
||||
class PostgameAdvancements(Toggle):
|
||||
"""Enables advancements that require spawning and defeating the Ender Dragon."""
|
||||
displayname = "Include Postgame Advancements"
|
||||
|
||||
|
||||
class SendDefeatedMobs(Toggle):
|
||||
"""Send killed mobs to other Minecraft worlds which have this option enabled."""
|
||||
displayname = "Send Defeated Mobs"
|
||||
|
||||
|
||||
minecraft_options: typing.Dict[str, type(Option)] = {
|
||||
"advancement_goal": AdvancementGoal,
|
||||
"combat_difficulty": CombatDifficulty,
|
||||
"include_hard_advancements": Toggle,
|
||||
"include_insane_advancements": Toggle,
|
||||
"include_postgame_advancements": Toggle,
|
||||
"shuffle_structures": Toggle,
|
||||
"structure_compasses": Toggle,
|
||||
"bee_traps": BeeTraps,
|
||||
"egg_shards_required": EggShards,
|
||||
"egg_shards_available": EggShards
|
||||
}
|
||||
"advancement_goal": AdvancementGoal,
|
||||
"egg_shards_required": EggShardsRequired,
|
||||
"egg_shards_available": EggShardsAvailable,
|
||||
"shuffle_structures": ShuffleStructures,
|
||||
"structure_compasses": StructureCompasses,
|
||||
"bee_traps": BeeTraps,
|
||||
"combat_difficulty": CombatDifficulty,
|
||||
"include_hard_advancements": HardAdvancements,
|
||||
"include_insane_advancements": InsaneAdvancements,
|
||||
"include_postgame_advancements": PostgameAdvancements,
|
||||
"send_defeated_mobs": SendDefeatedMobs,
|
||||
}
|
||||
|
||||
@@ -78,13 +78,13 @@ mandatory_connections = [
|
||||
('End Portal', 'The End')
|
||||
]
|
||||
|
||||
default_connections = {
|
||||
default_connections = [
|
||||
('Overworld Structure 1', 'Village'),
|
||||
('Overworld Structure 2', 'Pillager Outpost'),
|
||||
('Nether Structure 1', 'Nether Fortress'),
|
||||
('Nether Structure 2', 'Bastion Remnant'),
|
||||
('The End Structure', 'End City')
|
||||
}
|
||||
]
|
||||
|
||||
# Structure: illegal locations
|
||||
illegal_connections = {
|
||||
|
||||
@@ -31,7 +31,7 @@ class MinecraftLogic(LogicMixin):
|
||||
return self.can_reach('Nether Fortress', 'Region', player) and self._mc_basic_combat(player)
|
||||
|
||||
def _mc_can_brew_potions(self, player: int):
|
||||
return self._mc_fortress_loot(player) and self.has('Brewing', player) and self._mc_has_bottle(player)
|
||||
return self.has('Blaze Rods', player) and self.has('Brewing', player) and self._mc_has_bottle(player)
|
||||
|
||||
def _mc_can_piglin_trade(self, player: int):
|
||||
return self._mc_has_gold_ingots(player) and (
|
||||
@@ -39,7 +39,7 @@ class MinecraftLogic(LogicMixin):
|
||||
player))
|
||||
|
||||
def _mc_enter_stronghold(self, player: int):
|
||||
return self._mc_fortress_loot(player) and self.has('Brewing', player) and self.has('3 Ender Pearls', player)
|
||||
return self.has('Blaze Rods', player) and self.has('Brewing', player) and self.has('3 Ender Pearls', player)
|
||||
|
||||
# Difficulty-dependent functions
|
||||
def _mc_combat_difficulty(self, player: int):
|
||||
@@ -135,6 +135,7 @@ def set_rules(world: MultiWorld, player: int):
|
||||
set_rule(world.get_entrance("The End Structure", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("The End Structure", player))
|
||||
|
||||
set_rule(world.get_location("Ender Dragon", player), lambda state: can_complete(state))
|
||||
set_rule(world.get_location("Blaze Spawner", player), lambda state: state._mc_fortress_loot(player))
|
||||
|
||||
set_rule(world.get_location("Who is Cutting Onions?", player), lambda state: state._mc_can_piglin_trade(player))
|
||||
set_rule(world.get_location("Oh Shiny", player), lambda state: state._mc_can_piglin_trade(player))
|
||||
|
||||
@@ -16,6 +16,12 @@ from ..AutoWorld import World
|
||||
client_version = 6
|
||||
|
||||
class MinecraftWorld(World):
|
||||
"""
|
||||
Minecraft is a game about creativity. In a world made entirely of cubes, you explore, discover, mine,
|
||||
craft, and try not to explode. Delve deep into the earth and discover abandoned mines, ancient
|
||||
structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim
|
||||
victory!
|
||||
"""
|
||||
game: str = "Minecraft"
|
||||
options = minecraft_options
|
||||
topology_present = True
|
||||
@@ -37,6 +43,7 @@ class MinecraftWorld(World):
|
||||
'advancement_goal': self.world.advancement_goal[self.player],
|
||||
'egg_shards_required': self.world.egg_shards_required[self.player],
|
||||
'egg_shards_available': self.world.egg_shards_available[self.player],
|
||||
'MC35': bool(self.world.send_defeated_mobs[self.player]),
|
||||
'race': self.world.is_race
|
||||
}
|
||||
|
||||
@@ -46,7 +53,7 @@ class MinecraftWorld(World):
|
||||
itempool = []
|
||||
junk_pool = junk_weights.copy()
|
||||
# Add all required progression items
|
||||
for (name, num) in required_items.items():
|
||||
for (name, num) in required_items.items():
|
||||
itempool += [name] * num
|
||||
# Add structure compasses if desired
|
||||
if self.world.structure_compasses[self.player]:
|
||||
@@ -71,9 +78,9 @@ class MinecraftWorld(World):
|
||||
exclusion_pool.update(exclusion_table[key])
|
||||
exclusion_rules(self.world, self.player, exclusion_pool)
|
||||
|
||||
# Prefill the Ender Dragon with the completion condition
|
||||
completion = self.create_item("Victory")
|
||||
self.world.get_location("Ender Dragon", self.player).place_locked_item(completion)
|
||||
# Prefill event locations with their events
|
||||
self.world.get_location("Blaze Spawner", self.player).place_locked_item(self.create_item("Blaze Rods"))
|
||||
self.world.get_location("Ender Dragon", self.player).place_locked_item(self.create_item("Victory"))
|
||||
|
||||
self.world.itempool += itempool
|
||||
|
||||
@@ -84,9 +91,9 @@ class MinecraftWorld(World):
|
||||
def MCRegion(region_name: str, exits=[]):
|
||||
ret = Region(region_name, None, region_name, self.player, self.world)
|
||||
ret.locations = [MinecraftAdvancement(self.player, loc_name, loc_data.id, ret)
|
||||
for loc_name, loc_data in advancement_table.items()
|
||||
for loc_name, loc_data in advancement_table.items()
|
||||
if loc_data.region == region_name]
|
||||
for exit in exits:
|
||||
for exit in exits:
|
||||
ret.exits.append(Entrance(self.player, exit, ret))
|
||||
return ret
|
||||
|
||||
@@ -99,7 +106,7 @@ class MinecraftWorld(World):
|
||||
with open(os.path.join(output_directory, filename), 'wb') as f:
|
||||
f.write(b64encode(bytes(json.dumps(data), 'utf-8')))
|
||||
|
||||
def fill_slot_data(self):
|
||||
def fill_slot_data(self):
|
||||
slot_data = self._get_mc_data()
|
||||
for option_name in minecraft_options:
|
||||
option = getattr(self.world, option_name)[self.player]
|
||||
@@ -114,7 +121,7 @@ class MinecraftWorld(World):
|
||||
item.never_exclude = True
|
||||
return item
|
||||
|
||||
def mc_update_output(raw_data, server, port):
|
||||
def mc_update_output(raw_data, server, port):
|
||||
data = json.loads(b64decode(raw_data))
|
||||
data['server'] = server
|
||||
data['port'] = port
|
||||
|
||||
403
worlds/oot/Colors.py
Normal file
403
worlds/oot/Colors.py
Normal file
@@ -0,0 +1,403 @@
|
||||
from collections import namedtuple
|
||||
import random
|
||||
import re
|
||||
|
||||
Color = namedtuple('Color', ' R G B')
|
||||
|
||||
tunic_colors = {
|
||||
"Kokiri Green": Color(0x1E, 0x69, 0x1B),
|
||||
"Goron Red": Color(0x64, 0x14, 0x00),
|
||||
"Zora Blue": Color(0x00, 0x3C, 0x64),
|
||||
"Black": Color(0x30, 0x30, 0x30),
|
||||
"White": Color(0xF0, 0xF0, 0xFF),
|
||||
"Azure Blue": Color(0x13, 0x9E, 0xD8),
|
||||
"Vivid Cyan": Color(0x13, 0xE9, 0xD8),
|
||||
"Light Red": Color(0xF8, 0x7C, 0x6D),
|
||||
"Fuchsia": Color(0xFF, 0x00, 0xFF),
|
||||
"Purple": Color(0x95, 0x30, 0x80),
|
||||
"Majora Purple": Color(0x40, 0x00, 0x40),
|
||||
"Twitch Purple": Color(0x64, 0x41, 0xA5),
|
||||
"Purple Heart": Color(0x8A, 0x2B, 0xE2),
|
||||
"Persian Rose": Color(0xFF, 0x14, 0x93),
|
||||
"Dirty Yellow": Color(0xE0, 0xD8, 0x60),
|
||||
"Blush Pink": Color(0xF8, 0x6C, 0xF8),
|
||||
"Hot Pink": Color(0xFF, 0x69, 0xB4),
|
||||
"Rose Pink": Color(0xFF, 0x90, 0xB3),
|
||||
"Orange": Color(0xE0, 0x79, 0x40),
|
||||
"Gray": Color(0xA0, 0xA0, 0xB0),
|
||||
"Gold": Color(0xD8, 0xB0, 0x60),
|
||||
"Silver": Color(0xD0, 0xF0, 0xFF),
|
||||
"Beige": Color(0xC0, 0xA0, 0xA0),
|
||||
"Teal": Color(0x30, 0xD0, 0xB0),
|
||||
"Blood Red": Color(0x83, 0x03, 0x03),
|
||||
"Blood Orange": Color(0xFE, 0x4B, 0x03),
|
||||
"Royal Blue": Color(0x40, 0x00, 0x90),
|
||||
"Sonic Blue": Color(0x50, 0x90, 0xE0),
|
||||
"NES Green": Color(0x00, 0xD0, 0x00),
|
||||
"Dark Green": Color(0x00, 0x25, 0x18),
|
||||
"Lumen": Color(0x50, 0x8C, 0xF0),
|
||||
}
|
||||
|
||||
NaviColors = { # Inner Core Color Outer Glow Color
|
||||
"Rainbow": (Color(0x00, 0x00, 0x00), Color(0x00, 0x00, 0x00)),
|
||||
"Gold": (Color(0xFE, 0xCC, 0x3C), Color(0xFE, 0xC0, 0x07)),
|
||||
"White": (Color(0xFF, 0xFF, 0xFF), Color(0x00, 0x00, 0xFF)),
|
||||
"Green": (Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00)),
|
||||
"Light Blue": (Color(0x96, 0x96, 0xFF), Color(0x96, 0x96, 0xFF)),
|
||||
"Yellow": (Color(0xFF, 0xFF, 0x00), Color(0xC8, 0x9B, 0x00)),
|
||||
"Red": (Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00)),
|
||||
"Magenta": (Color(0xFF, 0x00, 0xFF), Color(0xC8, 0x00, 0x9B)),
|
||||
"Black": (Color(0x00, 0x00, 0x00), Color(0x00, 0x00, 0x00)),
|
||||
"Tatl": (Color(0xFF, 0xFF, 0xFF), Color(0xC8, 0x98, 0x00)),
|
||||
"Tael": (Color(0x49, 0x14, 0x6C), Color(0xFF, 0x00, 0x00)),
|
||||
"Fi": (Color(0x2C, 0x9E, 0xC4), Color(0x2C, 0x19, 0x83)),
|
||||
"Ciela": (Color(0xE6, 0xDE, 0x83), Color(0xC6, 0xBE, 0x5B)),
|
||||
"Epona": (Color(0xD1, 0x49, 0x02), Color(0x55, 0x1F, 0x08)),
|
||||
"Ezlo": (Color(0x62, 0x9C, 0x5F), Color(0x3F, 0x5D, 0x37)),
|
||||
"King of Red Lions": (Color(0xA8, 0x33, 0x17), Color(0xDE, 0xD7, 0xC5)),
|
||||
"Linebeck": (Color(0x03, 0x26, 0x60), Color(0xEF, 0xFF, 0xFF)),
|
||||
"Loftwing": (Color(0xD6, 0x2E, 0x31), Color(0xFD, 0xE6, 0xCC)),
|
||||
"Midna": (Color(0x19, 0x24, 0x26), Color(0xD2, 0x83, 0x30)),
|
||||
"Phantom Zelda": (Color(0x97, 0x7A, 0x6C), Color(0x6F, 0x46, 0x67)),
|
||||
}
|
||||
|
||||
sword_trail_colors = {
|
||||
"Rainbow": Color(0x00, 0x00, 0x00),
|
||||
"White": Color(0xFF, 0xFF, 0xFF),
|
||||
"Red": Color(0xFF, 0x00, 0x00),
|
||||
"Green": Color(0x00, 0xFF, 0x00),
|
||||
"Blue": Color(0x00, 0x00, 0xFF),
|
||||
"Cyan": Color(0x00, 0xFF, 0xFF),
|
||||
"Magenta": Color(0xFF, 0x00, 0xFF),
|
||||
"Orange": Color(0xFF, 0xA5, 0x00),
|
||||
"Gold": Color(0xFF, 0xD7, 0x00),
|
||||
"Purple": Color(0x80, 0x00, 0x80),
|
||||
"Pink": Color(0xFF, 0x69, 0xB4),
|
||||
}
|
||||
|
||||
bombchu_trail_colors = {
|
||||
"Rainbow": Color(0x00, 0x00, 0x00),
|
||||
"Red": Color(0xFA, 0x00, 0x00),
|
||||
"Green": Color(0x00, 0xFF, 0x00),
|
||||
"Blue": Color(0x00, 0x00, 0xFF),
|
||||
"Cyan": Color(0x00, 0xFF, 0xFF),
|
||||
"Magenta": Color(0xFF, 0x00, 0xFF),
|
||||
"Orange": Color(0xFF, 0xA5, 0x00),
|
||||
"Gold": Color(0xFF, 0xD7, 0x00),
|
||||
"Purple": Color(0x80, 0x00, 0x80),
|
||||
"Pink": Color(0xFF, 0x69, 0xB4),
|
||||
}
|
||||
|
||||
boomerang_trail_colors = {
|
||||
"Rainbow": Color(0x00, 0x00, 0x00),
|
||||
"Yellow": Color(0xFF, 0xFF, 0x64),
|
||||
"Red": Color(0xFF, 0x00, 0x00),
|
||||
"Green": Color(0x00, 0xFF, 0x00),
|
||||
"Blue": Color(0x00, 0x00, 0xFF),
|
||||
"Cyan": Color(0x00, 0xFF, 0xFF),
|
||||
"Magenta": Color(0xFF, 0x00, 0xFF),
|
||||
"Orange": Color(0xFF, 0xA5, 0x00),
|
||||
"Gold": Color(0xFF, 0xD7, 0x00),
|
||||
"Purple": Color(0x80, 0x00, 0x80),
|
||||
"Pink": Color(0xFF, 0x69, 0xB4),
|
||||
}
|
||||
|
||||
gauntlet_colors = {
|
||||
"Silver": Color(0xFF, 0xFF, 0xFF),
|
||||
"Gold": Color(0xFE, 0xCF, 0x0F),
|
||||
"Black": Color(0x00, 0x00, 0x06),
|
||||
"Green": Color(0x02, 0x59, 0x18),
|
||||
"Blue": Color(0x06, 0x02, 0x5A),
|
||||
"Bronze": Color(0x60, 0x06, 0x02),
|
||||
"Red": Color(0xFF, 0x00, 0x00),
|
||||
"Sky Blue": Color(0x02, 0x5D, 0xB0),
|
||||
"Pink": Color(0xFA, 0x6A, 0x90),
|
||||
"Magenta": Color(0xFF, 0x00, 0xFF),
|
||||
"Orange": Color(0xDA, 0x38, 0x00),
|
||||
"Lime": Color(0x5B, 0xA8, 0x06),
|
||||
"Purple": Color(0x80, 0x00, 0x80),
|
||||
}
|
||||
|
||||
shield_frame_colors = {
|
||||
"Red": Color(0xD7, 0x00, 0x00),
|
||||
"Green": Color(0x00, 0xFF, 0x00),
|
||||
"Blue": Color(0x00, 0x40, 0xD8),
|
||||
"Yellow": Color(0xFF, 0xFF, 0x64),
|
||||
"Cyan": Color(0x00, 0xFF, 0xFF),
|
||||
"Magenta": Color(0xFF, 0x00, 0xFF),
|
||||
"Orange": Color(0xFF, 0xA5, 0x00),
|
||||
"Gold": Color(0xFF, 0xD7, 0x00),
|
||||
"Purple": Color(0x80, 0x00, 0x80),
|
||||
"Pink": Color(0xFF, 0x69, 0xB4),
|
||||
}
|
||||
|
||||
heart_colors = {
|
||||
"Red": Color(0xFF, 0x46, 0x32),
|
||||
"Green": Color(0x46, 0xC8, 0x32),
|
||||
"Blue": Color(0x32, 0x46, 0xFF),
|
||||
"Yellow": Color(0xFF, 0xE0, 0x00),
|
||||
}
|
||||
|
||||
magic_colors = {
|
||||
"Green": Color(0x00, 0xC8, 0x00),
|
||||
"Red": Color(0xC8, 0x00, 0x00),
|
||||
"Blue": Color(0x00, 0x30, 0xFF),
|
||||
"Purple": Color(0xB0, 0x00, 0xFF),
|
||||
"Pink": Color(0xFF, 0x00, 0xC8),
|
||||
"Yellow": Color(0xFF, 0xFF, 0x00),
|
||||
"White": Color(0xFF, 0xFF, 0xFF),
|
||||
}
|
||||
|
||||
# A Button Text Cursor Shop Cursor Save/Death Cursor
|
||||
# Pause Menu A Cursor Pause Menu A Icon A Note
|
||||
a_button_colors = {
|
||||
"N64 Blue": (Color(0x5A, 0x5A, 0xFF), Color(0x00, 0x50, 0xC8), Color(0x00, 0x50, 0xFF), Color(0x64, 0x64, 0xFF),
|
||||
Color(0x00, 0x32, 0xFF), Color(0x00, 0x64, 0xFF), Color(0x50, 0x96, 0xFF)),
|
||||
"N64 Green": (Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00), Color(0x64, 0x96, 0x64),
|
||||
Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00)),
|
||||
"N64 Red": (Color(0xC8, 0x00, 0x00), Color(0xC8, 0x00, 0x00), Color(0xC8, 0x00, 0x00), Color(0xC8, 0x64, 0x64),
|
||||
Color(0xC8, 0x00, 0x00), Color(0xC8, 0x00, 0x00), Color(0xC8, 0x00, 0x00)),
|
||||
"GameCube Green": (Color(0x00, 0xC8, 0x32), Color(0x00, 0xC8, 0x50), Color(0x00, 0xFF, 0x50), Color(0x64, 0xFF, 0x64),
|
||||
Color(0x00, 0xFF, 0x32), Color(0x00, 0xFF, 0x64), Color(0x50, 0xFF, 0x96)),
|
||||
"GameCube Red": (Color(0xFF, 0x1E, 0x1E), Color(0xC8, 0x00, 0x00), Color(0xFF, 0x00, 0x50), Color(0xFF, 0x64, 0x64),
|
||||
Color(0xFF, 0x1E, 0x1E), Color(0xFF, 0x1E, 0x1E), Color(0xFF, 0x1E, 0x1E)),
|
||||
"GameCube Grey": (Color(0x78, 0x78, 0x78), Color(0x78, 0x78, 0x78), Color(0x78, 0x78, 0x78), Color(0x78, 0x78, 0x78),
|
||||
Color(0x78, 0x78, 0x78), Color(0x78, 0x78, 0x78), Color(0x78, 0x78, 0x78)),
|
||||
"Yellow": (Color(0xFF, 0xA0, 0x00), Color(0xFF, 0xA0, 0x00), Color(0xFF, 0xA0, 0x00), Color(0xFF, 0xA0, 0x00),
|
||||
Color(0xFF, 0xFF, 0x00), Color(0xFF, 0x96, 0x00), Color(0xFF, 0xFF, 0x32)),
|
||||
"Black": (Color(0x10, 0x10, 0x10), Color(0x00, 0x00, 0x00), Color(0x00, 0x00, 0x00), Color(0x10, 0x10, 0x10),
|
||||
Color(0x00, 0x00, 0x00), Color(0x18, 0x18, 0x18), Color(0x18, 0x18, 0x18)),
|
||||
"White": (Color(0xFF, 0xFF, 0xFF), Color(0xFF, 0xFF, 0xFF), Color(0xFF, 0xFF, 0xFF), Color(0xFF, 0xFF, 0xFF),
|
||||
Color(0xFF, 0xFF, 0xFF), Color(0xFF, 0xFF, 0xFF), Color(0xFF, 0xFF, 0xFF)),
|
||||
"Magenta": (Color(0xFF, 0x00, 0xFF), Color(0xFF, 0x00, 0xFF), Color(0xFF, 0x00, 0xFF), Color(0xFF, 0x00, 0xFF),
|
||||
Color(0xFF, 0x00, 0xFF), Color(0xFF, 0x00, 0xFF), Color(0xFF, 0x00, 0xFF)),
|
||||
"Ruby": (Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00),
|
||||
Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00)),
|
||||
"Sapphire": (Color(0x00, 0x00, 0xFF), Color(0x00, 0x00, 0xFF), Color(0x00, 0x00, 0xFF), Color(0x00, 0x00, 0xFF),
|
||||
Color(0x00, 0x00, 0xFF), Color(0x00, 0x00, 0xFF), Color(0x00, 0x00, 0xFF)),
|
||||
"Lime": (Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00),
|
||||
Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00)),
|
||||
"Cyan": (Color(0x00, 0xFF, 0xFF), Color(0x00, 0xFF, 0xFF), Color(0x00, 0xFF, 0xFF), Color(0x00, 0xFF, 0xFF),
|
||||
Color(0x00, 0xFF, 0xFF), Color(0x00, 0xFF, 0xFF), Color(0x00, 0xFF, 0xFF)),
|
||||
"Purple": (Color(0x80, 0x00, 0x80), Color(0x80, 0x00, 0x80), Color(0x80, 0x00, 0x80), Color(0x80, 0x00, 0x80),
|
||||
Color(0x80, 0x00, 0x80), Color(0x80, 0x00, 0x80), Color(0x80, 0x00, 0x80)),
|
||||
"Orange": (Color(0xFF, 0x80, 0x00), Color(0xFF, 0x80, 0x00), Color(0xFF, 0x80, 0x00), Color(0xFF, 0x80, 0x00),
|
||||
Color(0xFF, 0x80, 0x00), Color(0xFF, 0x80, 0x00), Color(0xFF, 0x80, 0x00)),
|
||||
}
|
||||
|
||||
# B Button
|
||||
b_button_colors = {
|
||||
"N64 Blue": Color(0x5A, 0x5A, 0xFF),
|
||||
"N64 Green": Color(0x00, 0x96, 0x00),
|
||||
"N64 Red": Color(0xC8, 0x00, 0x00),
|
||||
"GameCube Green": Color(0x00, 0xC8, 0x32),
|
||||
"GameCube Red": Color(0xFF, 0x1E, 0x1E),
|
||||
"GameCube Grey": Color(0x78, 0x78, 0x78),
|
||||
"Yellow": Color(0xFF, 0xA0, 0x00),
|
||||
"Black": Color(0x10, 0x10, 0x10),
|
||||
"White": Color(0xFF, 0xFF, 0xFF),
|
||||
"Magenta": Color(0xFF, 0x00, 0xFF),
|
||||
"Ruby": Color(0xFF, 0x00, 0x00),
|
||||
"Sapphire": Color(0x00, 0x00, 0xFF),
|
||||
"Lime": Color(0x00, 0xFF, 0x00),
|
||||
"Cyan": Color(0x00, 0xFF, 0xFF),
|
||||
"Purple": Color(0x80, 0x00, 0x80),
|
||||
"Orange": Color(0xFF, 0x80, 0x00),
|
||||
}
|
||||
|
||||
# C Button Pause Menu C Cursor Pause Menu C Icon C Note
|
||||
c_button_colors = {
|
||||
"N64 Blue": (Color(0x5A, 0x5A, 0xFF), Color(0x00, 0x32, 0xFF), Color(0x00, 0x64, 0xFF), Color(0x50, 0x96, 0xFF)),
|
||||
"N64 Green": (Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00)),
|
||||
"N64 Red": (Color(0xC8, 0x00, 0x00), Color(0xC8, 0x00, 0x00), Color(0xC8, 0x00, 0x00), Color(0xC8, 0x00, 0x00)),
|
||||
"GameCube Green": (Color(0x00, 0xC8, 0x32), Color(0x00, 0xFF, 0x32), Color(0x00, 0xFF, 0x64), Color(0x50, 0xFF, 0x96)),
|
||||
"GameCube Red": (Color(0xFF, 0x1E, 0x1E), Color(0xFF, 0x1E, 0x1E), Color(0xFF, 0x1E, 0x1E), Color(0xFF, 0x1E, 0x1E)),
|
||||
"GameCube Grey": (Color(0x78, 0x78, 0x78), Color(0x78, 0x78, 0x78), Color(0x78, 0x78, 0x78), Color(0x78, 0x78, 0x78)),
|
||||
"Yellow": (Color(0xFF, 0xA0, 0x00), Color(0xFF, 0xFF, 0x00), Color(0xFF, 0x96, 0x00), Color(0xFF, 0xFF, 0x32)),
|
||||
"Black": (Color(0x10, 0x10, 0x10), Color(0x00, 0x00, 0x00), Color(0x18, 0x18, 0x18), Color(0x18, 0x18, 0x18)),
|
||||
"White": (Color(0xFF, 0xFF, 0xFF), Color(0xFF, 0xFF, 0xFF), Color(0xFF, 0xFF, 0xFF), Color(0xFF, 0xFF, 0xFF)),
|
||||
"Magenta": (Color(0xFF, 0x00, 0xFF), Color(0xFF, 0x00, 0xFF), Color(0xFF, 0x00, 0xFF), Color(0xFF, 0x00, 0xFF)),
|
||||
"Ruby": (Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00)),
|
||||
"Sapphire": (Color(0x00, 0x00, 0xFF), Color(0x00, 0x00, 0xFF), Color(0x00, 0x00, 0xFF), Color(0x00, 0x00, 0xFF)),
|
||||
"Lime": (Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00)),
|
||||
"Cyan": (Color(0x00, 0xFF, 0xFF), Color(0x00, 0xFF, 0xFF), Color(0x00, 0xFF, 0xFF), Color(0x00, 0xFF, 0xFF)),
|
||||
"Purple": (Color(0x80, 0x00, 0x80), Color(0x80, 0x00, 0x80), Color(0x80, 0x00, 0x80), Color(0x80, 0x00, 0x80)),
|
||||
"Orange": (Color(0xFF, 0x80, 0x00), Color(0xFF, 0x80, 0x00), Color(0xFF, 0x80, 0x00), Color(0xFF, 0x80, 0x00)),
|
||||
}
|
||||
|
||||
# Start Button
|
||||
start_button_colors = {
|
||||
"N64 Blue": Color(0x5A, 0x5A, 0xFF),
|
||||
"N64 Green": Color(0x00, 0x96, 0x00),
|
||||
"N64 Red": Color(0xC8, 0x00, 0x00),
|
||||
"GameCube Green": Color(0x00, 0xC8, 0x32),
|
||||
"GameCube Red": Color(0xFF, 0x1E, 0x1E),
|
||||
"GameCube Grey": Color(0x78, 0x78, 0x78),
|
||||
"Yellow": Color(0xFF, 0xA0, 0x00),
|
||||
"Black": Color(0x10, 0x10, 0x10),
|
||||
"White": Color(0xFF, 0xFF, 0xFF),
|
||||
"Magenta": Color(0xFF, 0x00, 0xFF),
|
||||
"Ruby": Color(0xFF, 0x00, 0x00),
|
||||
"Sapphire": Color(0x00, 0x00, 0xFF),
|
||||
"Lime": Color(0x00, 0xFF, 0x00),
|
||||
"Cyan": Color(0x00, 0xFF, 0xFF),
|
||||
"Purple": Color(0x80, 0x00, 0x80),
|
||||
"Orange": Color(0xFF, 0x80, 0x00),
|
||||
}
|
||||
|
||||
meta_color_choices = ["Random Choice", "Completely Random"] #, "Custom Color"]
|
||||
|
||||
|
||||
def get_tunic_colors():
|
||||
return list(tunic_colors.keys())
|
||||
|
||||
|
||||
def get_tunic_color_options():
|
||||
return meta_color_choices + get_tunic_colors()
|
||||
|
||||
|
||||
def get_navi_colors():
|
||||
return list(NaviColors.keys())
|
||||
|
||||
|
||||
def get_navi_color_options(outer=False):
|
||||
if outer:
|
||||
return ["[Same as Inner]"] + meta_color_choices + get_navi_colors()
|
||||
else:
|
||||
return meta_color_choices + get_navi_colors()
|
||||
|
||||
|
||||
def get_sword_trail_colors():
|
||||
return list(sword_trail_colors.keys())
|
||||
|
||||
|
||||
def get_sword_trail_color_options(outer=False):
|
||||
if outer:
|
||||
return ["[Same as Inner]"] + meta_color_choices + get_sword_trail_colors()
|
||||
else:
|
||||
return meta_color_choices + get_sword_trail_colors()
|
||||
|
||||
|
||||
def get_bombchu_trail_colors():
|
||||
return list(bombchu_trail_colors.keys())
|
||||
|
||||
|
||||
def get_bombchu_trail_color_options(outer=False):
|
||||
if outer:
|
||||
return ["[Same as Inner]"] + meta_color_choices + get_bombchu_trail_colors()
|
||||
else:
|
||||
return meta_color_choices + get_bombchu_trail_colors()
|
||||
|
||||
|
||||
def get_boomerang_trail_colors():
|
||||
return list(boomerang_trail_colors.keys())
|
||||
|
||||
|
||||
def get_boomerang_trail_color_options(outer=False):
|
||||
if outer:
|
||||
return ["[Same as Inner]"] + meta_color_choices + get_boomerang_trail_colors()
|
||||
else:
|
||||
return meta_color_choices + get_boomerang_trail_colors()
|
||||
|
||||
|
||||
def get_gauntlet_colors():
|
||||
return list(gauntlet_colors.keys())
|
||||
|
||||
|
||||
def get_gauntlet_color_options():
|
||||
return meta_color_choices + get_gauntlet_colors()
|
||||
|
||||
|
||||
def get_shield_frame_colors():
|
||||
return list(shield_frame_colors.keys())
|
||||
|
||||
|
||||
def get_shield_frame_color_options():
|
||||
return meta_color_choices + get_shield_frame_colors()
|
||||
|
||||
|
||||
def get_heart_colors():
|
||||
return list(heart_colors.keys())
|
||||
|
||||
|
||||
def get_heart_color_options():
|
||||
return meta_color_choices + get_heart_colors()
|
||||
|
||||
|
||||
def get_magic_colors():
|
||||
return list(magic_colors.keys())
|
||||
|
||||
|
||||
def get_magic_color_options():
|
||||
return meta_color_choices + get_magic_colors()
|
||||
|
||||
|
||||
def get_a_button_colors():
|
||||
return list(a_button_colors.keys())
|
||||
|
||||
|
||||
def get_a_button_color_options():
|
||||
return meta_color_choices + get_a_button_colors()
|
||||
|
||||
|
||||
def get_b_button_colors():
|
||||
return list(b_button_colors.keys())
|
||||
|
||||
|
||||
def get_b_button_color_options():
|
||||
return meta_color_choices + get_b_button_colors()
|
||||
|
||||
|
||||
def get_c_button_colors():
|
||||
return list(c_button_colors.keys())
|
||||
|
||||
|
||||
def get_c_button_color_options():
|
||||
return meta_color_choices + get_c_button_colors()
|
||||
|
||||
|
||||
def get_start_button_colors():
|
||||
return list(start_button_colors.keys())
|
||||
|
||||
|
||||
def get_start_button_color_options():
|
||||
return meta_color_choices + get_start_button_colors()
|
||||
|
||||
|
||||
def contrast_ratio(color1, color2):
|
||||
# Based on accessibility standards (WCAG 2.0)
|
||||
lum1 = relative_luminance(color1)
|
||||
lum2 = relative_luminance(color2)
|
||||
return (max(lum1, lum2) + 0.05) / (min(lum1, lum2) + 0.05)
|
||||
|
||||
|
||||
def relative_luminance(color):
|
||||
color_ratios = list(map(lum_color_ratio, color))
|
||||
return color_ratios[0] * 0.299 + color_ratios[1] * 0.587 + color_ratios[2] * 0.114
|
||||
|
||||
|
||||
def lum_color_ratio(val):
|
||||
val /= 255
|
||||
if val <= 0.03928:
|
||||
return val / 12.92
|
||||
else:
|
||||
return pow((val + 0.055) / 1.055, 2.4)
|
||||
|
||||
|
||||
def generate_random_color():
|
||||
return [random.getrandbits(8), random.getrandbits(8), random.getrandbits(8)]
|
||||
|
||||
|
||||
def hex_to_color(option):
|
||||
# build color from hex code
|
||||
option = option[1:] if option[0] == "#" else option
|
||||
if not re.search(r'^(?:[0-9a-fA-F]{3}){1,2}$', option):
|
||||
raise Exception(f"Invalid color value provided: {option}")
|
||||
if len(option) > 3:
|
||||
return list(int(option[i:i + 2], 16) for i in (0, 2, 4))
|
||||
else:
|
||||
return list(int(f'{option[i]}{option[i]}', 16) for i in (0, 1, 2))
|
||||
|
||||
|
||||
def color_to_hex(color):
|
||||
return '#' + ''.join(['{:02X}'.format(c) for c in color])
|
||||
814
worlds/oot/Cosmetics.py
Normal file
814
worlds/oot/Cosmetics.py
Normal file
@@ -0,0 +1,814 @@
|
||||
from .Utils import data_path, __version__
|
||||
from .Colors import *
|
||||
import logging
|
||||
import worlds.oot.Music as music
|
||||
import worlds.oot.Sounds as sfx
|
||||
import worlds.oot.IconManip as icon
|
||||
from .JSONDump import dump_obj, CollapseList, CollapseDict, AlignedDict, SortedDict
|
||||
import json
|
||||
|
||||
logger = logging.getLogger('')
|
||||
|
||||
# Options are all lowercase and have underscores instead of spaces
|
||||
# this needs to be undone for the oot generator
|
||||
def format_cosmetic_option_result(option_result):
|
||||
def format_word(word):
|
||||
special_words = {
|
||||
'nes': 'NES',
|
||||
'gamecube': 'GameCube',
|
||||
'of': 'of'
|
||||
}
|
||||
return special_words.get(word, word.capitalize())
|
||||
words = option_result.split('_')
|
||||
return ' '.join([format_word(word) for word in words])
|
||||
|
||||
|
||||
def patch_targeting(rom, ootworld, symbols):
|
||||
# Set default targeting option to Hold
|
||||
if ootworld.default_targeting == 'hold':
|
||||
rom.write_byte(0xB71E6D, 0x01)
|
||||
else:
|
||||
rom.write_byte(0xB71E6D, 0x00)
|
||||
|
||||
|
||||
def patch_dpad(rom, ootworld, symbols):
|
||||
# Display D-Pad HUD
|
||||
if ootworld.display_dpad:
|
||||
rom.write_byte(symbols['CFG_DISPLAY_DPAD'], 0x01)
|
||||
else:
|
||||
rom.write_byte(symbols['CFG_DISPLAY_DPAD'], 0x00)
|
||||
|
||||
|
||||
|
||||
def patch_music(rom, ootworld, symbols):
|
||||
# patch music
|
||||
if ootworld.background_music != 'normal' or ootworld.fanfares != 'normal':
|
||||
music.restore_music(rom)
|
||||
log, errors = music.randomize_music(rom, ootworld, {})
|
||||
if errors:
|
||||
logger.error(errors)
|
||||
else:
|
||||
music.restore_music(rom)
|
||||
|
||||
|
||||
def patch_model_colors(rom, color, model_addresses):
|
||||
main_addresses, dark_addresses = model_addresses
|
||||
|
||||
if color is None:
|
||||
for address in main_addresses + dark_addresses:
|
||||
original = rom.original.read_bytes(address, 3)
|
||||
rom.write_bytes(address, original)
|
||||
return
|
||||
|
||||
for address in main_addresses:
|
||||
rom.write_bytes(address, color)
|
||||
|
||||
darkened_color = list(map(lambda light: int(max((light - 0x32) * 0.6, 0)), color))
|
||||
for address in dark_addresses:
|
||||
rom.write_bytes(address, darkened_color)
|
||||
|
||||
|
||||
def patch_tunic_icon(rom, tunic, color):
|
||||
# patch tunic icon colors
|
||||
icon_locations = {
|
||||
'Kokiri Tunic': 0x007FE000,
|
||||
'Goron Tunic': 0x007FF000,
|
||||
'Zora Tunic': 0x00800000,
|
||||
}
|
||||
|
||||
if color is not None:
|
||||
tunic_icon = icon.generate_tunic_icon(color)
|
||||
else:
|
||||
tunic_icon = rom.original.read_bytes(icon_locations[tunic], 0x1000)
|
||||
|
||||
rom.write_bytes(icon_locations[tunic], tunic_icon)
|
||||
|
||||
|
||||
def patch_tunic_colors(rom, ootworld, symbols):
|
||||
# patch tunic colors
|
||||
tunics = [
|
||||
('Kokiri Tunic', 'kokiri_color', 0x00B6DA38),
|
||||
('Goron Tunic', 'goron_color', 0x00B6DA3B),
|
||||
('Zora Tunic', 'zora_color', 0x00B6DA3E),
|
||||
]
|
||||
tunic_color_list = get_tunic_colors()
|
||||
|
||||
for tunic, tunic_setting, address in tunics:
|
||||
tunic_option = format_cosmetic_option_result(ootworld.__dict__[tunic_setting])
|
||||
|
||||
# handle random
|
||||
if tunic_option == 'Random Choice':
|
||||
tunic_option = random.choice(tunic_color_list)
|
||||
# handle completely random
|
||||
if tunic_option == 'Completely Random':
|
||||
color = generate_random_color()
|
||||
# grab the color from the list
|
||||
elif tunic_option in tunic_colors:
|
||||
color = list(tunic_colors[tunic_option])
|
||||
# build color from hex code
|
||||
else:
|
||||
color = hex_to_color(tunic_option)
|
||||
tunic_option = 'Custom'
|
||||
# "Weird" weirdshots will crash if the Kokiri Tunic Green value is > 0x99. Brickwall it.
|
||||
if ootworld.logic_rules != 'glitchless' and tunic == 'Kokiri Tunic':
|
||||
color[1] = min(color[1],0x98)
|
||||
rom.write_bytes(address, color)
|
||||
|
||||
# patch the tunic icon
|
||||
if [tunic, tunic_option] not in [['Kokiri Tunic', 'Kokiri Green'], ['Goron Tunic', 'Goron Red'], ['Zora Tunic', 'Zora Blue']]:
|
||||
patch_tunic_icon(rom, tunic, color)
|
||||
else:
|
||||
patch_tunic_icon(rom, tunic, None)
|
||||
|
||||
|
||||
def patch_navi_colors(rom, ootworld, symbols):
|
||||
# patch navi colors
|
||||
navi = [
|
||||
# colors for Navi
|
||||
('Navi Idle', 'navi_color_default',
|
||||
[0x00B5E184], # Default (Player)
|
||||
symbols.get('CFG_RAINBOW_NAVI_IDLE_INNER_ENABLED', None), symbols.get('CFG_RAINBOW_NAVI_IDLE_OUTER_ENABLED', None)),
|
||||
('Navi Targeting Enemy', 'navi_color_enemy',
|
||||
[0x00B5E19C, 0x00B5E1BC], # Enemy, Boss
|
||||
symbols.get('CFG_RAINBOW_NAVI_ENEMY_INNER_ENABLED', None), symbols.get('CFG_RAINBOW_NAVI_ENEMY_OUTER_ENABLED', None)),
|
||||
('Navi Targeting NPC', 'navi_color_npc',
|
||||
[0x00B5E194], # NPC
|
||||
symbols.get('CFG_RAINBOW_NAVI_NPC_INNER_ENABLED', None), symbols.get('CFG_RAINBOW_NAVI_NPC_OUTER_ENABLED', None)),
|
||||
('Navi Targeting Prop', 'navi_color_prop',
|
||||
[0x00B5E174, 0x00B5E17C, 0x00B5E18C, 0x00B5E1A4, 0x00B5E1AC,
|
||||
0x00B5E1B4, 0x00B5E1C4, 0x00B5E1CC, 0x00B5E1D4], # Everything else
|
||||
symbols.get('CFG_RAINBOW_NAVI_PROP_INNER_ENABLED', None), symbols.get('CFG_RAINBOW_NAVI_PROP_OUTER_ENABLED', None)),
|
||||
]
|
||||
|
||||
navi_color_list = get_navi_colors()
|
||||
rainbow_error = None
|
||||
|
||||
for navi_action, navi_setting, navi_addresses, rainbow_inner_symbol, rainbow_outer_symbol in navi:
|
||||
navi_option_inner = format_cosmetic_option_result(ootworld.__dict__[navi_setting+'_inner'])
|
||||
navi_option_outer = format_cosmetic_option_result(ootworld.__dict__[navi_setting+'_outer'])
|
||||
|
||||
# choose a random choice for the whole group
|
||||
if navi_option_inner == 'Random Choice':
|
||||
navi_option_inner = random.choice(navi_color_list)
|
||||
if navi_option_outer == 'Random Choice':
|
||||
navi_option_outer = random.choice(navi_color_list)
|
||||
|
||||
if navi_option_outer == 'Match Inner':
|
||||
navi_option_outer = navi_option_inner
|
||||
|
||||
colors = []
|
||||
option_dict = {}
|
||||
for address_index, address in enumerate(navi_addresses):
|
||||
address_colors = {}
|
||||
colors.append(address_colors)
|
||||
for index, (navi_part, option, rainbow_symbol) in enumerate([
|
||||
('inner', navi_option_inner, rainbow_inner_symbol),
|
||||
('outer', navi_option_outer, rainbow_outer_symbol),
|
||||
]):
|
||||
color = None
|
||||
|
||||
# set rainbow option
|
||||
if rainbow_symbol is not None and option == 'Rainbow':
|
||||
rom.write_byte(rainbow_symbol, 0x01)
|
||||
color = [0x00, 0x00, 0x00]
|
||||
elif rainbow_symbol is not None:
|
||||
rom.write_byte(rainbow_symbol, 0x00)
|
||||
elif option == 'Rainbow':
|
||||
rainbow_error = "Rainbow Navi is not supported by this patch version. Using 'Completely Random' as a substitute."
|
||||
option = 'Completely Random'
|
||||
|
||||
# completely random is random for every subgroup
|
||||
if color is None and option == 'Completely Random':
|
||||
color = generate_random_color()
|
||||
|
||||
# grab the color from the list
|
||||
if color is None and option in NaviColors:
|
||||
color = list(NaviColors[option][index])
|
||||
|
||||
# build color from hex code
|
||||
if color is None:
|
||||
color = hex_to_color(option)
|
||||
option = 'Custom'
|
||||
|
||||
# Check color validity
|
||||
if color is None:
|
||||
raise Exception(f'Invalid {navi_part} color {option} for {navi_action}')
|
||||
|
||||
address_colors[navi_part] = color
|
||||
option_dict[navi_part] = option
|
||||
|
||||
# write color
|
||||
color = address_colors['inner'] + [0xFF] + address_colors['outer'] + [0xFF]
|
||||
rom.write_bytes(address, color)
|
||||
|
||||
|
||||
if rainbow_error:
|
||||
logger.error(rainbow_error)
|
||||
|
||||
|
||||
def patch_sword_trails(rom, ootworld, symbols):
|
||||
# patch sword trail duration
|
||||
rom.write_byte(0x00BEFF8C, ootworld.sword_trail_duration)
|
||||
|
||||
# patch sword trail colors
|
||||
sword_trails = [
|
||||
('Sword Trail', 'sword_trail_color',
|
||||
[(0x00BEFF7C, 0xB0, 0x40, 0xB0, 0xFF), (0x00BEFF84, 0x20, 0x00, 0x10, 0x00)],
|
||||
symbols.get('CFG_RAINBOW_SWORD_INNER_ENABLED', None), symbols.get('CFG_RAINBOW_SWORD_OUTER_ENABLED', None)),
|
||||
]
|
||||
|
||||
sword_trail_color_list = get_sword_trail_colors()
|
||||
rainbow_error = None
|
||||
|
||||
for trail_name, trail_setting, trail_addresses, rainbow_inner_symbol, rainbow_outer_symbol in sword_trails:
|
||||
option_inner = format_cosmetic_option_result(ootworld.__dict__[trail_setting+'_inner'])
|
||||
option_outer = format_cosmetic_option_result(ootworld.__dict__[trail_setting+'_outer'])
|
||||
|
||||
# handle random choice
|
||||
if option_inner == 'Random Choice':
|
||||
option_inner = random.choice(sword_trail_color_list)
|
||||
if option_outer == 'Random Choice':
|
||||
option_outer = random.choice(sword_trail_color_list)
|
||||
|
||||
if option_outer == 'Match Inner':
|
||||
option_outer = option_inner
|
||||
|
||||
colors = []
|
||||
option_dict = {}
|
||||
for address_index, (address, inner_transparency, inner_white_transparency, outer_transparency, outer_white_transparency) in enumerate(trail_addresses):
|
||||
address_colors = {}
|
||||
colors.append(address_colors)
|
||||
transparency_dict = {}
|
||||
for index, (trail_part, option, rainbow_symbol, white_transparency, transparency) in enumerate([
|
||||
('inner', option_inner, rainbow_inner_symbol, inner_white_transparency, inner_transparency),
|
||||
('outer', option_outer, rainbow_outer_symbol, outer_white_transparency, outer_transparency),
|
||||
]):
|
||||
color = None
|
||||
|
||||
# set rainbow option
|
||||
if rainbow_symbol is not None and option == 'Rainbow':
|
||||
rom.write_byte(rainbow_symbol, 0x01)
|
||||
color = [0x00, 0x00, 0x00]
|
||||
elif rainbow_symbol is not None:
|
||||
rom.write_byte(rainbow_symbol, 0x00)
|
||||
elif option == 'Rainbow':
|
||||
rainbow_error = "Rainbow Sword Trail is not supported by this patch version. Using 'Completely Random' as a substitute."
|
||||
option = 'Completely Random'
|
||||
|
||||
# completely random is random for every subgroup
|
||||
if color is None and option == 'Completely Random':
|
||||
color = generate_random_color()
|
||||
|
||||
# grab the color from the list
|
||||
if color is None and option in sword_trail_colors:
|
||||
color = list(sword_trail_colors[option])
|
||||
|
||||
# build color from hex code
|
||||
if color is None:
|
||||
color = hex_to_color(option)
|
||||
option = 'Custom'
|
||||
|
||||
# Check color validity
|
||||
if color is None:
|
||||
raise Exception(f'Invalid {trail_part} color {option} for {trail_name}')
|
||||
|
||||
# handle white transparency
|
||||
if option == 'White':
|
||||
transparency_dict[trail_part] = white_transparency
|
||||
else:
|
||||
transparency_dict[trail_part] = transparency
|
||||
|
||||
address_colors[trail_part] = color
|
||||
option_dict[trail_part] = option
|
||||
|
||||
# write color
|
||||
color = address_colors['outer'] + [transparency_dict['outer']] + address_colors['inner'] + [transparency_dict['inner']]
|
||||
rom.write_bytes(address, color)
|
||||
|
||||
if rainbow_error:
|
||||
logger.error(rainbow_error)
|
||||
|
||||
|
||||
def patch_bombchu_trails(rom, ootworld, symbols):
|
||||
# patch bombchu trail colors
|
||||
bombchu_trails = [
|
||||
('Bombchu Trail', 'bombchu_trail_color', get_bombchu_trail_colors(), bombchu_trail_colors,
|
||||
(symbols['CFG_BOMBCHU_TRAIL_INNER_COLOR'], symbols['CFG_BOMBCHU_TRAIL_OUTER_COLOR'],
|
||||
symbols['CFG_RAINBOW_BOMBCHU_TRAIL_INNER_ENABLED'], symbols['CFG_RAINBOW_BOMBCHU_TRAIL_OUTER_ENABLED'])),
|
||||
]
|
||||
|
||||
patch_trails(rom, ootworld, bombchu_trails)
|
||||
|
||||
|
||||
def patch_boomerang_trails(rom, ootworld, symbols):
|
||||
# patch boomerang trail colors
|
||||
boomerang_trails = [
|
||||
('Boomerang Trail', 'boomerang_trail_color', get_boomerang_trail_colors(), boomerang_trail_colors,
|
||||
(symbols['CFG_BOOM_TRAIL_INNER_COLOR'], symbols['CFG_BOOM_TRAIL_OUTER_COLOR'],
|
||||
symbols['CFG_RAINBOW_BOOM_TRAIL_INNER_ENABLED'], symbols['CFG_RAINBOW_BOOM_TRAIL_OUTER_ENABLED'])),
|
||||
]
|
||||
|
||||
patch_trails(rom, ootworld, boomerang_trails)
|
||||
|
||||
|
||||
def patch_trails(rom, ootworld, trails):
|
||||
for trail_name, trail_setting, trail_color_list, trail_color_dict, trail_symbols in trails:
|
||||
color_inner_symbol, color_outer_symbol, rainbow_inner_symbol, rainbow_outer_symbol = trail_symbols
|
||||
option_inner = format_cosmetic_option_result(ootworld.__dict__[trail_setting+'_inner'])
|
||||
option_outer = format_cosmetic_option_result(ootworld.__dict__[trail_setting+'_outer'])
|
||||
|
||||
# handle random choice
|
||||
if option_inner == 'Random Choice':
|
||||
option_inner = random.choice(trail_color_list)
|
||||
if option_outer == 'Random Choice':
|
||||
option_outer = random.choice(trail_color_list)
|
||||
|
||||
if option_outer == 'Match Inner':
|
||||
option_outer = option_inner
|
||||
|
||||
option_dict = {}
|
||||
colors = {}
|
||||
|
||||
for index, (trail_part, option, rainbow_symbol, color_symbol) in enumerate([
|
||||
('inner', option_inner, rainbow_inner_symbol, color_inner_symbol),
|
||||
('outer', option_outer, rainbow_outer_symbol, color_outer_symbol),
|
||||
]):
|
||||
color = None
|
||||
|
||||
# set rainbow option
|
||||
if option == 'Rainbow':
|
||||
rom.write_byte(rainbow_symbol, 0x01)
|
||||
color = [0x00, 0x00, 0x00]
|
||||
else:
|
||||
rom.write_byte(rainbow_symbol, 0x00)
|
||||
|
||||
# handle completely random
|
||||
if color is None and option == 'Completely Random':
|
||||
# Specific handling for inner bombchu trails for contrast purposes.
|
||||
if trail_name == 'Bombchu Trail' and trail_part == 'inner':
|
||||
fixed_dark_color = [0, 0, 0]
|
||||
color = [0, 0, 0]
|
||||
# Avoid colors which have a low contrast so the bombchu ticking is still visible
|
||||
while contrast_ratio(color, fixed_dark_color) <= 4:
|
||||
color = generate_random_color()
|
||||
else:
|
||||
color = generate_random_color()
|
||||
|
||||
# grab the color from the list
|
||||
if color is None and option in trail_color_dict:
|
||||
color = list(trail_color_dict[option])
|
||||
|
||||
# build color from hex code
|
||||
if color is None:
|
||||
color = hex_to_color(option)
|
||||
option = 'Custom'
|
||||
|
||||
option_dict[trail_part] = option
|
||||
colors[trail_part] = color
|
||||
|
||||
# write color
|
||||
rom.write_bytes(color_symbol, color)
|
||||
|
||||
|
||||
|
||||
def patch_gauntlet_colors(rom, ootworld, symbols):
|
||||
# patch gauntlet colors
|
||||
gauntlets = [
|
||||
('Silver Gauntlets', 'silver_gauntlets_color', 0x00B6DA44,
|
||||
([0x173B4CC], [0x173B4D4, 0x173B50C, 0x173B514])), # GI Model DList colors
|
||||
('Gold Gauntlets', 'golden_gauntlets_color', 0x00B6DA47,
|
||||
([0x173B4EC], [0x173B4F4, 0x173B52C, 0x173B534])), # GI Model DList colors
|
||||
]
|
||||
gauntlet_color_list = get_gauntlet_colors()
|
||||
|
||||
for gauntlet, gauntlet_setting, address, model_addresses in gauntlets:
|
||||
gauntlet_option = format_cosmetic_option_result(ootworld.__dict__[gauntlet_setting])
|
||||
|
||||
# handle random
|
||||
if gauntlet_option == 'Random Choice':
|
||||
gauntlet_option = random.choice(gauntlet_color_list)
|
||||
# handle completely random
|
||||
if gauntlet_option == 'Completely Random':
|
||||
color = generate_random_color()
|
||||
# grab the color from the list
|
||||
elif gauntlet_option in gauntlet_colors:
|
||||
color = list(gauntlet_colors[gauntlet_option])
|
||||
# build color from hex code
|
||||
else:
|
||||
color = hex_to_color(gauntlet_option)
|
||||
gauntlet_option = 'Custom'
|
||||
rom.write_bytes(address, color)
|
||||
if ootworld.correct_model_colors:
|
||||
patch_model_colors(rom, color, model_addresses)
|
||||
else:
|
||||
patch_model_colors(rom, None, model_addresses)
|
||||
|
||||
def patch_shield_frame_colors(rom, ootworld, symbols):
|
||||
# patch shield frame colors
|
||||
shield_frames = [
|
||||
('Mirror Shield Frame', 'mirror_shield_frame_color',
|
||||
[0xFA7274, 0xFA776C, 0xFAA27C, 0xFAC564, 0xFAC984, 0xFAEDD4],
|
||||
([0x1616FCC], [0x1616FD4])),
|
||||
]
|
||||
shield_frame_color_list = get_shield_frame_colors()
|
||||
|
||||
for shield_frame, shield_frame_setting, addresses, model_addresses in shield_frames:
|
||||
shield_frame_option = format_cosmetic_option_result(ootworld.__dict__[shield_frame_setting])
|
||||
|
||||
# handle random
|
||||
if shield_frame_option == 'Random Choice':
|
||||
shield_frame_option = random.choice(shield_frame_color_list)
|
||||
# handle completely random
|
||||
if shield_frame_option == 'Completely Random':
|
||||
color = [random.getrandbits(8), random.getrandbits(8), random.getrandbits(8)]
|
||||
# grab the color from the list
|
||||
elif shield_frame_option in shield_frame_colors:
|
||||
color = list(shield_frame_colors[shield_frame_option])
|
||||
# build color from hex code
|
||||
else:
|
||||
color = hex_to_color(shield_frame_option)
|
||||
shield_frame_option = 'Custom'
|
||||
for address in addresses:
|
||||
rom.write_bytes(address, color)
|
||||
if ootworld.correct_model_colors and shield_frame_option != 'Red':
|
||||
patch_model_colors(rom, color, model_addresses)
|
||||
else:
|
||||
patch_model_colors(rom, None, model_addresses)
|
||||
|
||||
|
||||
def patch_heart_colors(rom, ootworld, symbols):
|
||||
# patch heart colors
|
||||
hearts = [
|
||||
('Heart Color', 'heart_color', symbols['CFG_HEART_COLOR'], 0xBB0994,
|
||||
([0x14DA474, 0x14DA594, 0x14B701C, 0x14B70DC],
|
||||
[0x14B70FC, 0x14DA494, 0x14DA5B4, 0x14B700C, 0x14B702C, 0x14B703C, 0x14B704C, 0x14B705C,
|
||||
0x14B706C, 0x14B707C, 0x14B708C, 0x14B709C, 0x14B70AC, 0x14B70BC, 0x14B70CC])), # GI Model DList colors
|
||||
]
|
||||
heart_color_list = get_heart_colors()
|
||||
|
||||
for heart, heart_setting, symbol, file_select_address, model_addresses in hearts:
|
||||
heart_option = format_cosmetic_option_result(ootworld.__dict__[heart_setting])
|
||||
|
||||
# handle random
|
||||
if heart_option == 'Random Choice':
|
||||
heart_option = random.choice(heart_color_list)
|
||||
# handle completely random
|
||||
if heart_option == 'Completely Random':
|
||||
color = generate_random_color()
|
||||
# grab the color from the list
|
||||
elif heart_option in heart_colors:
|
||||
color = list(heart_colors[heart_option])
|
||||
# build color from hex code
|
||||
else:
|
||||
color = hex_to_color(heart_option)
|
||||
heart_option = 'Custom'
|
||||
rom.write_int16s(symbol, color) # symbol for ingame HUD
|
||||
rom.write_int16s(file_select_address, color) # file select normal hearts
|
||||
if heart_option != 'Red':
|
||||
rom.write_int16s(file_select_address + 6, color) # file select DD hearts
|
||||
else:
|
||||
original_dd_color = rom.original.read_bytes(file_select_address + 6, 6)
|
||||
rom.write_bytes(file_select_address + 6, original_dd_color)
|
||||
if ootworld.correct_model_colors and heart_option != 'Red':
|
||||
patch_model_colors(rom, color, model_addresses) # heart model colors
|
||||
icon.patch_overworld_icon(rom, color, 0xF43D80) # Overworld Heart Icon
|
||||
else:
|
||||
patch_model_colors(rom, None, model_addresses)
|
||||
icon.patch_overworld_icon(rom, None, 0xF43D80)
|
||||
|
||||
def patch_magic_colors(rom, ootworld, symbols):
|
||||
# patch magic colors
|
||||
magic = [
|
||||
('Magic Meter Color', 'magic_color', symbols["CFG_MAGIC_COLOR"],
|
||||
([0x154C654, 0x154CFB4], [0x154C65C, 0x154CFBC])), # GI Model DList colors
|
||||
]
|
||||
magic_color_list = get_magic_colors()
|
||||
|
||||
for magic_color, magic_setting, symbol, model_addresses in magic:
|
||||
magic_option = format_cosmetic_option_result(ootworld.__dict__[magic_setting])
|
||||
|
||||
if magic_option == 'Random Choice':
|
||||
magic_option = random.choice(magic_color_list)
|
||||
|
||||
if magic_option == 'Completely Random':
|
||||
color = generate_random_color()
|
||||
elif magic_option in magic_colors:
|
||||
color = list(magic_colors[magic_option])
|
||||
else:
|
||||
color = hex_to_color(magic_option)
|
||||
magic_option = 'Custom'
|
||||
rom.write_int16s(symbol, color)
|
||||
if magic_option != 'Green' and ootworld.correct_model_colors:
|
||||
patch_model_colors(rom, color, model_addresses)
|
||||
icon.patch_overworld_icon(rom, color, 0xF45650, data_path('icons/magicSmallExtras.raw')) # Overworld Small Pot
|
||||
icon.patch_overworld_icon(rom, color, 0xF47650, data_path('icons/magicLargeExtras.raw')) # Overworld Big Pot
|
||||
else:
|
||||
patch_model_colors(rom, None, model_addresses)
|
||||
icon.patch_overworld_icon(rom, None, 0xF45650)
|
||||
icon.patch_overworld_icon(rom, None, 0xF47650)
|
||||
|
||||
def patch_button_colors(rom, ootworld, symbols):
|
||||
buttons = [
|
||||
('A Button Color', 'a_button_color', a_button_colors,
|
||||
[('A Button Color', symbols['CFG_A_BUTTON_COLOR'],
|
||||
None),
|
||||
('Text Cursor Color', symbols['CFG_TEXT_CURSOR_COLOR'],
|
||||
[(0xB88E81, 0xB88E85, 0xB88E9)]), # Initial Inner Color
|
||||
('Shop Cursor Color', symbols['CFG_SHOP_CURSOR_COLOR'],
|
||||
None),
|
||||
('Save/Death Cursor Color', None,
|
||||
[(0xBBEBC2, 0xBBEBC3, 0xBBEBD6), (0xBBEDDA, 0xBBEDDB, 0xBBEDDE)]), # Save Cursor / Death Cursor
|
||||
('Pause Menu A Cursor Color', None,
|
||||
[(0xBC7849, 0xBC784B, 0xBC784D), (0xBC78A9, 0xBC78AB, 0xBC78AD), (0xBC78BB, 0xBC78BD, 0xBC78BF)]), # Inner / Pulse 1 / Pulse 2
|
||||
('Pause Menu A Icon Color', None,
|
||||
[(0x845754, 0x845755, 0x845756)]),
|
||||
('A Note Color', symbols['CFG_A_NOTE_COLOR'], # For Textbox Song Display
|
||||
[(0xBB299A, 0xBB299B, 0xBB299E), (0xBB2C8E, 0xBB2C8F, 0xBB2C92), (0xBB2F8A, 0xBB2F8B, 0xBB2F96)]), # Pause Menu Song Display
|
||||
]),
|
||||
('B Button Color', 'b_button_color', b_button_colors,
|
||||
[('B Button Color', symbols['CFG_B_BUTTON_COLOR'],
|
||||
None),
|
||||
]),
|
||||
('C Button Color', 'c_button_color', c_button_colors,
|
||||
[('C Button Color', symbols['CFG_C_BUTTON_COLOR'],
|
||||
None),
|
||||
('Pause Menu C Cursor Color', None,
|
||||
[(0xBC7843, 0xBC7845, 0xBC7847), (0xBC7891, 0xBC7893, 0xBC7895), (0xBC78A3, 0xBC78A5, 0xBC78A7)]), # Inner / Pulse 1 / Pulse 2
|
||||
('Pause Menu C Icon Color', None,
|
||||
[(0x8456FC, 0x8456FD, 0x8456FE)]),
|
||||
('C Note Color', symbols['CFG_C_NOTE_COLOR'], # For Textbox Song Display
|
||||
[(0xBB2996, 0xBB2997, 0xBB29A2), (0xBB2C8A, 0xBB2C8B, 0xBB2C96), (0xBB2F86, 0xBB2F87, 0xBB2F9A)]), # Pause Menu Song Display
|
||||
]),
|
||||
('Start Button Color', 'start_button_color', start_button_colors,
|
||||
[('Start Button Color', None,
|
||||
[(0xAE9EC6, 0xAE9EC7, 0xAE9EDA)]),
|
||||
]),
|
||||
]
|
||||
|
||||
for button, button_setting, button_colors, patches in buttons:
|
||||
button_option = format_cosmetic_option_result(ootworld.__dict__[button_setting])
|
||||
color_set = None
|
||||
colors = {}
|
||||
|
||||
# handle random
|
||||
if button_option == 'Random Choice':
|
||||
button_option = random.choice(list(button_colors.keys()))
|
||||
# handle completely random
|
||||
if button_option == 'Completely Random':
|
||||
fixed_font_color = [10, 10, 10]
|
||||
color = [0, 0, 0]
|
||||
# Avoid colors which have a low contrast with the font inside buttons (eg. the A letter)
|
||||
while contrast_ratio(color, fixed_font_color) <= 3:
|
||||
color = generate_random_color()
|
||||
# grab the color from the list
|
||||
elif button_option in button_colors:
|
||||
color_set = [button_colors[button_option]] if isinstance(button_colors[button_option][0], int) else list(button_colors[button_option])
|
||||
color = color_set[0]
|
||||
# build color from hex code
|
||||
else:
|
||||
color = hex_to_color(button_option)
|
||||
button_option = 'Custom'
|
||||
|
||||
# apply all button color patches
|
||||
for i, (patch, symbol, byte_addresses) in enumerate(patches):
|
||||
if color_set is not None and len(color_set) > i and color_set[i]:
|
||||
colors[patch] = color_set[i]
|
||||
else:
|
||||
colors[patch] = color
|
||||
|
||||
if symbol:
|
||||
rom.write_int16s(symbol, colors[patch])
|
||||
|
||||
if byte_addresses:
|
||||
for r_addr, g_addr, b_addr in byte_addresses:
|
||||
rom.write_byte(r_addr, colors[patch][0])
|
||||
rom.write_byte(g_addr, colors[patch][1])
|
||||
rom.write_byte(b_addr, colors[patch][2])
|
||||
|
||||
|
||||
def patch_sfx(rom, ootworld, symbols):
|
||||
# Configurable Sound Effects
|
||||
sfx_config = [
|
||||
('sfx_navi_overworld', sfx.SoundHooks.NAVI_OVERWORLD),
|
||||
('sfx_navi_enemy', sfx.SoundHooks.NAVI_ENEMY),
|
||||
('sfx_low_hp', sfx.SoundHooks.HP_LOW),
|
||||
('sfx_menu_cursor', sfx.SoundHooks.MENU_CURSOR),
|
||||
('sfx_menu_select', sfx.SoundHooks.MENU_SELECT),
|
||||
('sfx_nightfall', sfx.SoundHooks.NIGHTFALL),
|
||||
('sfx_horse_neigh', sfx.SoundHooks.HORSE_NEIGH),
|
||||
('sfx_hover_boots', sfx.SoundHooks.BOOTS_HOVER),
|
||||
]
|
||||
sound_dict = sfx.get_patch_dict()
|
||||
sounds_keyword_label = {sound.value.keyword: sound.value.label for sound in sfx.Sounds}
|
||||
sounds_label_keyword = {sound.value.label: sound.value.keyword for sound in sfx.Sounds}
|
||||
|
||||
for setting, hook in sfx_config:
|
||||
selection = ootworld.__dict__[setting].replace('_', '-')
|
||||
|
||||
if selection == 'default':
|
||||
for loc in hook.value.locations:
|
||||
sound_id = rom.original.read_int16(loc)
|
||||
rom.write_int16(loc, sound_id)
|
||||
else:
|
||||
if selection == 'random-choice':
|
||||
selection = random.choice(sfx.get_hook_pool(hook)).value.keyword
|
||||
elif selection == 'random-ear-safe':
|
||||
selection = random.choice(sfx.get_hook_pool(hook, "TRUE")).value.keyword
|
||||
elif selection == 'completely-random':
|
||||
selection = random.choice(sfx.standard).value.keyword
|
||||
sound_id = sound_dict[selection]
|
||||
for loc in hook.value.locations:
|
||||
rom.write_int16(loc, sound_id)
|
||||
|
||||
|
||||
|
||||
def patch_instrument(rom, ootworld, symbols):
|
||||
# Player Instrument
|
||||
instruments = {
|
||||
#'none': 0x00,
|
||||
'ocarina': 0x01,
|
||||
'malon': 0x02,
|
||||
'whistle': 0x03,
|
||||
'harp': 0x04,
|
||||
'grind_organ': 0x05,
|
||||
'flute': 0x06,
|
||||
#'another_ocarina': 0x07,
|
||||
}
|
||||
|
||||
choice = ootworld.sfx_ocarina
|
||||
if choice == 'random-choice':
|
||||
choice = random.choice(list(instruments.keys()))
|
||||
|
||||
rom.write_byte(0x00B53C7B, instruments[choice])
|
||||
rom.write_byte(0x00B4BF6F, instruments[choice]) # For Lost Woods Skull Kids' minigame in Lost Woods
|
||||
|
||||
|
||||
legacy_cosmetic_data_headers = [
|
||||
0x03481000,
|
||||
0x03480810,
|
||||
]
|
||||
|
||||
global_patch_sets = [
|
||||
patch_targeting,
|
||||
patch_music,
|
||||
patch_tunic_colors,
|
||||
patch_navi_colors,
|
||||
patch_sword_trails,
|
||||
patch_gauntlet_colors,
|
||||
patch_shield_frame_colors,
|
||||
patch_sfx,
|
||||
patch_instrument,
|
||||
]
|
||||
|
||||
patch_sets = {
|
||||
0x1F04FA62: {
|
||||
"patches": [
|
||||
patch_dpad,
|
||||
patch_sword_trails,
|
||||
],
|
||||
"symbols": {
|
||||
"CFG_DISPLAY_DPAD": 0x0004,
|
||||
"CFG_RAINBOW_SWORD_INNER_ENABLED": 0x0005,
|
||||
"CFG_RAINBOW_SWORD_OUTER_ENABLED": 0x0006,
|
||||
},
|
||||
},
|
||||
0x1F05D3F9: {
|
||||
"patches": [
|
||||
patch_dpad,
|
||||
patch_sword_trails,
|
||||
],
|
||||
"symbols": {
|
||||
"CFG_DISPLAY_DPAD": 0x0004,
|
||||
"CFG_RAINBOW_SWORD_INNER_ENABLED": 0x0005,
|
||||
"CFG_RAINBOW_SWORD_OUTER_ENABLED": 0x0006,
|
||||
},
|
||||
},
|
||||
0x1F0693FB: {
|
||||
"patches": [
|
||||
patch_dpad,
|
||||
patch_sword_trails,
|
||||
patch_heart_colors,
|
||||
patch_magic_colors,
|
||||
],
|
||||
"symbols": {
|
||||
"CFG_MAGIC_COLOR": 0x0004,
|
||||
"CFG_HEART_COLOR": 0x000A,
|
||||
"CFG_DISPLAY_DPAD": 0x0010,
|
||||
"CFG_RAINBOW_SWORD_INNER_ENABLED": 0x0011,
|
||||
"CFG_RAINBOW_SWORD_OUTER_ENABLED": 0x0012,
|
||||
}
|
||||
},
|
||||
0x1F073FC9: {
|
||||
"patches": [
|
||||
patch_dpad,
|
||||
patch_sword_trails,
|
||||
patch_heart_colors,
|
||||
patch_magic_colors,
|
||||
patch_button_colors,
|
||||
],
|
||||
"symbols": {
|
||||
"CFG_MAGIC_COLOR": 0x0004,
|
||||
"CFG_HEART_COLOR": 0x000A,
|
||||
"CFG_A_BUTTON_COLOR": 0x0010,
|
||||
"CFG_B_BUTTON_COLOR": 0x0016,
|
||||
"CFG_C_BUTTON_COLOR": 0x001C,
|
||||
"CFG_TEXT_CURSOR_COLOR": 0x0022,
|
||||
"CFG_SHOP_CURSOR_COLOR": 0x0028,
|
||||
"CFG_A_NOTE_COLOR": 0x002E,
|
||||
"CFG_C_NOTE_COLOR": 0x0034,
|
||||
"CFG_DISPLAY_DPAD": 0x003A,
|
||||
"CFG_RAINBOW_SWORD_INNER_ENABLED": 0x003B,
|
||||
"CFG_RAINBOW_SWORD_OUTER_ENABLED": 0x003C,
|
||||
}
|
||||
},
|
||||
0x1F073FD8: {
|
||||
"patches": [
|
||||
patch_dpad,
|
||||
patch_navi_colors,
|
||||
patch_sword_trails,
|
||||
patch_heart_colors,
|
||||
patch_magic_colors,
|
||||
patch_button_colors,
|
||||
patch_boomerang_trails,
|
||||
patch_bombchu_trails,
|
||||
],
|
||||
"symbols": {
|
||||
"CFG_MAGIC_COLOR": 0x0004,
|
||||
"CFG_HEART_COLOR": 0x000A,
|
||||
"CFG_A_BUTTON_COLOR": 0x0010,
|
||||
"CFG_B_BUTTON_COLOR": 0x0016,
|
||||
"CFG_C_BUTTON_COLOR": 0x001C,
|
||||
"CFG_TEXT_CURSOR_COLOR": 0x0022,
|
||||
"CFG_SHOP_CURSOR_COLOR": 0x0028,
|
||||
"CFG_A_NOTE_COLOR": 0x002E,
|
||||
"CFG_C_NOTE_COLOR": 0x0034,
|
||||
"CFG_BOOM_TRAIL_INNER_COLOR": 0x003A,
|
||||
"CFG_BOOM_TRAIL_OUTER_COLOR": 0x003D,
|
||||
"CFG_BOMBCHU_TRAIL_INNER_COLOR": 0x0040,
|
||||
"CFG_BOMBCHU_TRAIL_OUTER_COLOR": 0x0043,
|
||||
"CFG_DISPLAY_DPAD": 0x0046,
|
||||
"CFG_RAINBOW_SWORD_INNER_ENABLED": 0x0047,
|
||||
"CFG_RAINBOW_SWORD_OUTER_ENABLED": 0x0048,
|
||||
"CFG_RAINBOW_BOOM_TRAIL_INNER_ENABLED": 0x0049,
|
||||
"CFG_RAINBOW_BOOM_TRAIL_OUTER_ENABLED": 0x004A,
|
||||
"CFG_RAINBOW_BOMBCHU_TRAIL_INNER_ENABLED": 0x004B,
|
||||
"CFG_RAINBOW_BOMBCHU_TRAIL_OUTER_ENABLED": 0x004C,
|
||||
"CFG_RAINBOW_NAVI_IDLE_INNER_ENABLED": 0x004D,
|
||||
"CFG_RAINBOW_NAVI_IDLE_OUTER_ENABLED": 0x004E,
|
||||
"CFG_RAINBOW_NAVI_ENEMY_INNER_ENABLED": 0x004F,
|
||||
"CFG_RAINBOW_NAVI_ENEMY_OUTER_ENABLED": 0x0050,
|
||||
"CFG_RAINBOW_NAVI_NPC_INNER_ENABLED": 0x0051,
|
||||
"CFG_RAINBOW_NAVI_NPC_OUTER_ENABLED": 0x0052,
|
||||
"CFG_RAINBOW_NAVI_PROP_INNER_ENABLED": 0x0053,
|
||||
"CFG_RAINBOW_NAVI_PROP_OUTER_ENABLED": 0x0054,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def patch_cosmetics(ootworld, rom):
|
||||
# Use the world's slot seed for cosmetics
|
||||
random.seed(ootworld.world.slot_seeds[ootworld.player])
|
||||
|
||||
# try to detect the cosmetic patch data format
|
||||
versioned_patch_set = None
|
||||
cosmetic_context = rom.read_int32(rom.sym('RANDO_CONTEXT') + 4)
|
||||
if cosmetic_context >= 0x80000000 and cosmetic_context <= 0x80F7FFFC:
|
||||
cosmetic_context = (cosmetic_context - 0x80400000) + 0x3480000 # convert from RAM to ROM address
|
||||
cosmetic_version = rom.read_int32(cosmetic_context)
|
||||
versioned_patch_set = patch_sets.get(cosmetic_version)
|
||||
else:
|
||||
# If cosmetic_context is not a valid pointer, then try to
|
||||
# search over all possible legacy header locations.
|
||||
for header in legacy_cosmetic_data_headers:
|
||||
cosmetic_context = header
|
||||
cosmetic_version = rom.read_int32(cosmetic_context)
|
||||
if cosmetic_version in patch_sets:
|
||||
versioned_patch_set = patch_sets[cosmetic_version]
|
||||
break
|
||||
|
||||
# patch version specific patches
|
||||
if versioned_patch_set:
|
||||
# offset the cosmetic_context struct for absolute addressing
|
||||
cosmetic_context_symbols = {
|
||||
sym: address + cosmetic_context
|
||||
for sym, address in versioned_patch_set['symbols'].items()
|
||||
}
|
||||
|
||||
# warn if patching a legacy format
|
||||
if cosmetic_version != rom.read_int32(rom.sym('COSMETIC_FORMAT_VERSION')):
|
||||
logger.error("ROM uses old cosmetic patch format.")
|
||||
|
||||
# patch cosmetics that use vanilla oot data, and always compatible
|
||||
for patch_func in [patch for patch in global_patch_sets if patch not in versioned_patch_set['patches']]:
|
||||
patch_func(rom, ootworld, {})
|
||||
|
||||
for patch_func in versioned_patch_set['patches']:
|
||||
patch_func(rom, ootworld, cosmetic_context_symbols)
|
||||
else:
|
||||
# patch cosmetics that use vanilla oot data, and always compatible
|
||||
for patch_func in global_patch_sets:
|
||||
patch_func(rom, ootworld, {})
|
||||
|
||||
# Unknown patch format
|
||||
logger.error("Unable to patch some cosmetics. ROM uses unknown cosmetic patch format.")
|
||||
56
worlds/oot/Dungeon.py
Normal file
56
worlds/oot/Dungeon.py
Normal file
@@ -0,0 +1,56 @@
|
||||
class Dungeon(object):
|
||||
|
||||
def __init__(self, world, name, hint, boss_key, small_keys, dungeon_items):
|
||||
def to_array(obj):
|
||||
if obj == None:
|
||||
return []
|
||||
if isinstance(obj, list):
|
||||
return obj
|
||||
else:
|
||||
return [obj]
|
||||
|
||||
self.world = world
|
||||
self.name = name
|
||||
self.hint_text = hint
|
||||
self.regions = []
|
||||
self.boss_key = to_array(boss_key)
|
||||
self.small_keys = to_array(small_keys)
|
||||
self.dungeon_items = to_array(dungeon_items)
|
||||
|
||||
for region in world.world.regions:
|
||||
if region.player == world.player and region.dungeon == self.name:
|
||||
region.dungeon = self
|
||||
self.regions.append(region)
|
||||
|
||||
|
||||
def copy(self, new_world):
|
||||
new_boss_key = [item.copy(new_world) for item in self.boss_key]
|
||||
new_small_keys = [item.copy(new_world) for item in self.small_keys]
|
||||
new_dungeon_items = [item.copy(new_world) for item in self.dungeon_items]
|
||||
|
||||
new_dungeon = Dungeon(new_world, self.name, self.hint, new_boss_key, new_small_keys, new_dungeon_items)
|
||||
|
||||
return new_dungeon
|
||||
|
||||
|
||||
@property
|
||||
def keys(self):
|
||||
return self.small_keys + self.boss_key
|
||||
|
||||
|
||||
@property
|
||||
def all_items(self):
|
||||
return self.dungeon_items + self.keys
|
||||
|
||||
|
||||
def is_dungeon_item(self, item):
|
||||
return item.name in [dungeon_item.name for dungeon_item in self.all_items]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__unicode__())
|
||||
|
||||
|
||||
def __unicode__(self):
|
||||
return '%s' % self.name
|
||||
|
||||
129
worlds/oot/DungeonList.py
Normal file
129
worlds/oot/DungeonList.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import os
|
||||
|
||||
from .Dungeon import Dungeon
|
||||
from .Utils import data_path
|
||||
|
||||
|
||||
dungeon_table = [
|
||||
{
|
||||
'name': 'Deku Tree',
|
||||
'boss_key': 0,
|
||||
'small_key': 0,
|
||||
'small_key_mq': 0,
|
||||
'dungeon_item': 1,
|
||||
},
|
||||
{
|
||||
'name': 'Dodongos Cavern',
|
||||
'hint': 'Dodongo\'s Cavern',
|
||||
'boss_key': 0,
|
||||
'small_key': 0,
|
||||
'small_key_mq': 0,
|
||||
'dungeon_item': 1,
|
||||
},
|
||||
{
|
||||
'name': 'Jabu Jabus Belly',
|
||||
'hint': 'Jabu Jabu\'s Belly',
|
||||
'boss_key': 0,
|
||||
'small_key': 0,
|
||||
'small_key_mq': 0,
|
||||
'dungeon_item': 1,
|
||||
},
|
||||
{
|
||||
'name': 'Forest Temple',
|
||||
'boss_key': 1,
|
||||
'small_key': 5,
|
||||
'small_key_mq': 6,
|
||||
'dungeon_item': 1,
|
||||
},
|
||||
{
|
||||
'name': 'Bottom of the Well',
|
||||
'boss_key': 0,
|
||||
'small_key': 3,
|
||||
'small_key_mq': 2,
|
||||
'dungeon_item': 1,
|
||||
},
|
||||
{
|
||||
'name': 'Fire Temple',
|
||||
'boss_key': 1,
|
||||
'small_key': 8,
|
||||
'small_key_mq': 5,
|
||||
'dungeon_item': 1,
|
||||
},
|
||||
{
|
||||
'name': 'Ice Cavern',
|
||||
'boss_key': 0,
|
||||
'small_key': 0,
|
||||
'small_key_mq': 0,
|
||||
'dungeon_item': 1,
|
||||
},
|
||||
{
|
||||
'name': 'Water Temple',
|
||||
'boss_key': 1,
|
||||
'small_key': 6,
|
||||
'small_key_mq': 2,
|
||||
'dungeon_item': 1,
|
||||
},
|
||||
{
|
||||
'name': 'Shadow Temple',
|
||||
'boss_key': 1,
|
||||
'small_key': 5,
|
||||
'small_key_mq': 6,
|
||||
'dungeon_item': 1,
|
||||
},
|
||||
{
|
||||
'name': 'Gerudo Training Grounds',
|
||||
'boss_key': 0,
|
||||
'small_key': 9,
|
||||
'small_key_mq': 3,
|
||||
'dungeon_item': 0,
|
||||
},
|
||||
{
|
||||
'name': 'Spirit Temple',
|
||||
'boss_key': 1,
|
||||
'small_key': 5,
|
||||
'small_key_mq': 7,
|
||||
'dungeon_item': 1,
|
||||
},
|
||||
{
|
||||
'name': 'Ganons Castle',
|
||||
'hint': 'Ganon\'s Castle',
|
||||
'boss_key': 1,
|
||||
'small_key': 2,
|
||||
'small_key_mq': 3,
|
||||
'dungeon_item': 0,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def create_dungeons(ootworld):
|
||||
ootworld.dungeons = []
|
||||
for dungeon_info in dungeon_table:
|
||||
name = dungeon_info['name']
|
||||
hint = dungeon_info['hint'] if 'hint' in dungeon_info else name
|
||||
|
||||
if ootworld.logic_rules == 'glitchless':
|
||||
if not ootworld.dungeon_mq[name]:
|
||||
dungeon_json = os.path.join(data_path('World'), name + '.json')
|
||||
else:
|
||||
dungeon_json = os.path.join(data_path('World'), name + ' MQ.json')
|
||||
else:
|
||||
if not ootworld.dungeon_mq[name]:
|
||||
dungeon_json = os.path.join(data_path('Glitched World'), name + '.json')
|
||||
else:
|
||||
dungeon_json = os.path.join(data_path('Glitched World'), name + ' MQ.json')
|
||||
|
||||
|
||||
ootworld.load_regions_from_json(dungeon_json)
|
||||
|
||||
boss_keys = [ootworld.create_item(f'Boss Key ({name})') for i in range(dungeon_info['boss_key'])]
|
||||
if not ootworld.dungeon_mq[dungeon_info['name']]:
|
||||
small_keys = [ootworld.create_item(f'Small Key ({name})') for i in range(dungeon_info['small_key'])]
|
||||
else:
|
||||
small_keys = [ootworld.create_item(f'Small Key ({name})') for i in range(dungeon_info['small_key_mq'])]
|
||||
dungeon_items = [ootworld.create_item(f'Map ({name})'), ootworld.create_item(f'Compass ({name})')] * dungeon_info['dungeon_item']
|
||||
if ootworld.shuffle_mapcompass in ['any_dungeon', 'overworld']:
|
||||
for item in dungeon_items:
|
||||
item.priority = True
|
||||
|
||||
ootworld.dungeons.append(Dungeon(ootworld, name, hint, boss_keys, small_keys, dungeon_items))
|
||||
|
||||
19
worlds/oot/Entrance.py
Normal file
19
worlds/oot/Entrance.py
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
from BaseClasses import Entrance
|
||||
from .Regions import TimeOfDay
|
||||
|
||||
class OOTEntrance(Entrance):
|
||||
game: str = 'Ocarina of Time'
|
||||
|
||||
def __init__(self, player, name='', parent=None):
|
||||
super(OOTEntrance, self).__init__(player, name, parent)
|
||||
self.access_rules = []
|
||||
self.reverse = None
|
||||
self.replaces = None
|
||||
self.assumed = None
|
||||
self.type = None
|
||||
self.shuffled = False
|
||||
self.data = None
|
||||
self.primary = False
|
||||
self.always = False
|
||||
self.never = False
|
||||
25
worlds/oot/EntranceShuffle.py
Normal file
25
worlds/oot/EntranceShuffle.py
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
def shuffle_random_entrances(ootworld):
|
||||
world = ootworld.world
|
||||
player = ootworld.player
|
||||
|
||||
# Gather locations to keep reachable for validation
|
||||
|
||||
# Set entrance data for all entrances
|
||||
|
||||
# Determine entrance pools based on settings
|
||||
|
||||
# Mark shuffled entrances
|
||||
|
||||
# Build target entrance pools
|
||||
|
||||
# Place priority entrances
|
||||
|
||||
# Delete priority targets from one-way pools
|
||||
|
||||
# Shuffle all entrance pools, in order
|
||||
|
||||
# Verification steps:
|
||||
# All entrances are properly connected to a region
|
||||
# Game is beatable
|
||||
# Validate world
|
||||
1292
worlds/oot/HintList.py
Normal file
1292
worlds/oot/HintList.py
Normal file
File diff suppressed because it is too large
Load Diff
1059
worlds/oot/Hints.py
Normal file
1059
worlds/oot/Hints.py
Normal file
File diff suppressed because it is too large
Load Diff
112
worlds/oot/IconManip.py
Normal file
112
worlds/oot/IconManip.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from .Utils import data_path
|
||||
|
||||
# TODO
|
||||
# Move the tunic to the generalized system
|
||||
|
||||
# Function for adding hue to a greyscaled icon
|
||||
def add_hue(image, color, tiff=False):
|
||||
start = 154 if tiff else 0
|
||||
for i in range(start, len(image), 4):
|
||||
try:
|
||||
for x in range(3):
|
||||
image[i+x] = int(((image[i+x]/255) * (color[x]/255)) * 255)
|
||||
except:
|
||||
pass
|
||||
return image
|
||||
|
||||
|
||||
# Function for adding belt to tunic
|
||||
def add_belt(tunic, belt, tiff=False):
|
||||
start = 154 if tiff else 0
|
||||
for i in range(start, len(tunic), 4):
|
||||
try:
|
||||
if belt[i+3] != 0:
|
||||
alpha = belt[i+3] / 255
|
||||
for x in range(3):
|
||||
tunic[i+x] = int((belt[i+x] * alpha) + (tunic[i+x] * (1 - alpha)))
|
||||
except:
|
||||
pass
|
||||
return tunic
|
||||
|
||||
|
||||
# Function for putting tunic colors together
|
||||
def generate_tunic_icon(color):
|
||||
with open(data_path('icons/grey.tiff'), 'rb') as grey_fil, open(data_path('icons/belt.tiff'), 'rb') as belt_fil:
|
||||
grey = list(grey_fil.read())
|
||||
belt = list(belt_fil.read())
|
||||
return add_belt(add_hue(grey, color, True), belt, True)[154:]
|
||||
|
||||
# END TODO
|
||||
|
||||
# Function to add extra data on top of icon
|
||||
def add_extra_data(rgbValues, fileName, intensity = 0.5):
|
||||
fileRGB = []
|
||||
with open(fileName, "rb") as fil:
|
||||
data = fil.read()
|
||||
for i in range(0, len(data), 4):
|
||||
fileRGB.append([data[i+0], data[i+1], data[i+2], data[i+3]])
|
||||
for i in range(len(rgbValues)):
|
||||
alpha = fileRGB[i][3] / 255
|
||||
for x in range(3):
|
||||
rgbValues[i][x] = int((fileRGB[i][x] * alpha + intensity) + (rgbValues[i][x] * (1 - alpha - intensity)))
|
||||
|
||||
# Function for desaturating RGB values
|
||||
def greyscaleRGB(rgbValues, intensity: int = 2):
|
||||
for rgb in rgbValues:
|
||||
rgb[0] = rgb[1] = rgb[2] = int((rgb[0] * 0.2126 + rgb[1] * 0.7152 + rgb[2] * 0.0722) * intensity)
|
||||
return rgbValues
|
||||
|
||||
# Converts rgb5a1 values to RGBA lists
|
||||
def rgb5a1ToRGB(rgb5a1Bytes):
|
||||
pixels = []
|
||||
for i in range(0, len(rgb5a1Bytes), 2):
|
||||
bits = format(rgb5a1Bytes[i], '#010b')[2:] + format(rgb5a1Bytes[i+1], '#010b')[2:]
|
||||
r = int(int(bits[0:5], 2) * (255/31))
|
||||
g = int(int(bits[5:10], 2) * (255/31))
|
||||
b = int(int(bits[10:15], 2) * (255/31))
|
||||
a = int(bits[15], 2) * 255
|
||||
pixels.append([r,g,b,a])
|
||||
return pixels
|
||||
|
||||
# Adds a hue to RGB values
|
||||
def addHueToRGB(rgbValues, color):
|
||||
for rgb in rgbValues:
|
||||
for i in range(3):
|
||||
rgb[i] = int(((rgb[i]/255) * (color[i]/255)) * 255)
|
||||
return rgbValues
|
||||
|
||||
# Convert RGB to RGB5a1 format
|
||||
def rgbToRGB5a1(rgbValues):
|
||||
rgb5a1 = []
|
||||
for rgb in rgbValues:
|
||||
r = int(rgb[0] / (255/31))
|
||||
r = r if r <= 31 else 31
|
||||
r = r if r >= 0 else 0
|
||||
g = int(rgb[1] / (255/31))
|
||||
g = g if g <= 31 else 31
|
||||
g = g if g >= 0 else 0
|
||||
b = int(rgb[2] / (255/31))
|
||||
b = b if b <= 31 else 31
|
||||
b = b if b >= 0 else 0
|
||||
a = int(rgb[3] / 255)
|
||||
bits = format(r, '#07b')[2:] + format(g, '#07b')[2:] + format(b, '#07b')[2:] + format(a, '#03b')[2:]
|
||||
rgb5a1.append(int(bits[:8], 2))
|
||||
rgb5a1.append(int(bits[8:], 2))
|
||||
for i in rgb5a1:
|
||||
assert i <= 255, i
|
||||
return bytes(rgb5a1)
|
||||
|
||||
# Patch overworld icons
|
||||
def patch_overworld_icon(rom, color, address, fileName = None):
|
||||
original = rom.original.read_bytes(address, 0x800)
|
||||
|
||||
if color is None:
|
||||
rom.write_bytes(address, original)
|
||||
return
|
||||
|
||||
rgbBytes = rgb5a1ToRGB(original)
|
||||
greyscaled = greyscaleRGB(rgbBytes)
|
||||
rgbBytes = addHueToRGB(greyscaled, color)
|
||||
if fileName != None:
|
||||
add_extra_data(rgbBytes, fileName)
|
||||
rom.write_bytes(address, rgbToRGB5a1(rgbBytes))
|
||||
1410
worlds/oot/ItemPool.py
Normal file
1410
worlds/oot/ItemPool.py
Normal file
File diff suppressed because it is too large
Load Diff
414
worlds/oot/Items.py
Normal file
414
worlds/oot/Items.py
Normal file
@@ -0,0 +1,414 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import Item
|
||||
|
||||
def oot_data_to_ap_id(data, event):
|
||||
if event or data[2] is None or data[0] == 'Shop':
|
||||
return None
|
||||
offset = 66000
|
||||
if data[0] in ['Item', 'BossKey', 'Compass', 'Map', 'SmallKey', 'Token', 'GanonBossKey', 'FortressSmallKey', 'Song']:
|
||||
return offset + data[2]
|
||||
else:
|
||||
raise Exception(f'Unexpected OOT item type found: {data[0]}')
|
||||
|
||||
def ap_id_to_oot_data(ap_id):
|
||||
offset = 66000
|
||||
val = ap_id - offset
|
||||
try:
|
||||
return list(filter(lambda d: d[1][0] == 'Item' and d[1][2] == val, item_table.items()))[0]
|
||||
except IndexError:
|
||||
raise Exception(f'Could not find desired item ID: {ap_id}')
|
||||
|
||||
class OOTItem(Item):
|
||||
game: str = "Ocarina of Time"
|
||||
|
||||
def __init__(self, name, player, data, event):
|
||||
(type, advancement, index, special) = data
|
||||
adv = True if advancement else False # this looks silly but the table uses True, False, and None
|
||||
super(OOTItem, self).__init__(name, adv, oot_data_to_ap_id(data, event), player)
|
||||
self.type = type
|
||||
self.index = index
|
||||
self.special = special or {}
|
||||
self.looks_like_item = None
|
||||
self.price = special.get('price', None) if special else None
|
||||
self.internal = False
|
||||
|
||||
# The playthrough calculation calls a function that uses "sweep_for_events(key_only=True)"
|
||||
# This checks if the item it's looking for is a small key, using the small key property.
|
||||
# Because of overlapping item fields, this means that OoT small keys are technically counted, unless we do this.
|
||||
# This causes them to be double-collected during playthrough and generation.
|
||||
@property
|
||||
def smallkey(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def bigkey(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def dungeonitem(self) -> bool:
|
||||
return self.type in ['SmallKey', 'FortressSmallKey', 'BossKey', 'GanonBossKey', 'Map', 'Compass']
|
||||
|
||||
|
||||
|
||||
# Progressive: True -> Advancement
|
||||
# False -> Priority
|
||||
# None -> Normal
|
||||
# Item: (type, Progressive, GetItemID, special),
|
||||
item_table = {
|
||||
'Bombs (5)': ('Item', None, 0x01, None),
|
||||
'Deku Nuts (5)': ('Item', None, 0x02, None),
|
||||
'Bombchus (10)': ('Item', True, 0x03, None),
|
||||
'Boomerang': ('Item', True, 0x06, None),
|
||||
'Deku Stick (1)': ('Item', None, 0x07, None),
|
||||
'Lens of Truth': ('Item', True, 0x0A, None),
|
||||
'Megaton Hammer': ('Item', True, 0x0D, None),
|
||||
'Cojiro': ('Item', True, 0x0E, None),
|
||||
'Bottle': ('Item', True, 0x0F, {'bottle': float('Inf')}),
|
||||
'Bottle with Milk': ('Item', True, 0x14, {'bottle': float('Inf')}),
|
||||
'Rutos Letter': ('Item', True, 0x15, None),
|
||||
'Deliver Letter': ('Item', True, None, {'bottle': float('Inf')}),
|
||||
'Sell Big Poe': ('Item', True, None, {'bottle': float('Inf')}),
|
||||
'Magic Bean': ('Item', True, 0x16, None),
|
||||
'Skull Mask': ('Item', True, 0x17, None),
|
||||
'Spooky Mask': ('Item', None, 0x18, None),
|
||||
'Keaton Mask': ('Item', None, 0x1A, None),
|
||||
'Bunny Hood': ('Item', None, 0x1B, None),
|
||||
'Mask of Truth': ('Item', True, 0x1C, None),
|
||||
'Pocket Egg': ('Item', True, 0x1D, None),
|
||||
'Pocket Cucco': ('Item', True, 0x1E, None),
|
||||
'Odd Mushroom': ('Item', True, 0x1F, None),
|
||||
'Odd Potion': ('Item', True, 0x20, None),
|
||||
'Poachers Saw': ('Item', True, 0x21, None),
|
||||
'Broken Sword': ('Item', True, 0x22, None),
|
||||
'Prescription': ('Item', True, 0x23, None),
|
||||
'Eyeball Frog': ('Item', True, 0x24, None),
|
||||
'Eyedrops': ('Item', True, 0x25, None),
|
||||
'Claim Check': ('Item', True, 0x26, None),
|
||||
'Kokiri Sword': ('Item', True, 0x27, None),
|
||||
'Giants Knife': ('Item', True, 0x28, None),
|
||||
'Deku Shield': ('Item', None, 0x29, None),
|
||||
'Hylian Shield': ('Item', None, 0x2A, None),
|
||||
'Mirror Shield': ('Item', True, 0x2B, None),
|
||||
'Goron Tunic': ('Item', True, 0x2C, None),
|
||||
'Zora Tunic': ('Item', True, 0x2D, None),
|
||||
'Iron Boots': ('Item', True, 0x2E, None),
|
||||
'Hover Boots': ('Item', True, 0x2F, None),
|
||||
'Stone of Agony': ('Item', True, 0x39, None),
|
||||
'Gerudo Membership Card': ('Item', True, 0x3A, None),
|
||||
'Heart Container': ('Item', None, 0x3D, None),
|
||||
'Piece of Heart': ('Item', None, 0x3E, None),
|
||||
'Boss Key': ('BossKey', True, 0x3F, None),
|
||||
'Compass': ('Compass', None, 0x40, None),
|
||||
'Map': ('Map', None, 0x41, None),
|
||||
'Small Key': ('SmallKey', True, 0x42, {'progressive': float('Inf')}),
|
||||
'Weird Egg': ('Item', True, 0x47, None),
|
||||
'Recovery Heart': ('Item', None, 0x48, None),
|
||||
'Arrows (5)': ('Item', None, 0x49, None),
|
||||
'Arrows (10)': ('Item', None, 0x4A, None),
|
||||
'Arrows (30)': ('Item', None, 0x4B, None),
|
||||
'Rupee (1)': ('Item', None, 0x4C, None),
|
||||
'Rupees (5)': ('Item', None, 0x4D, None),
|
||||
'Rupees (20)': ('Item', None, 0x4E, None),
|
||||
'Heart Container (Boss)': ('Item', None, 0x4F, None),
|
||||
'Milk': ('Item', None, 0x50, None),
|
||||
'Goron Mask': ('Item', None, 0x51, None),
|
||||
'Zora Mask': ('Item', None, 0x52, None),
|
||||
'Gerudo Mask': ('Item', None, 0x53, None),
|
||||
'Rupees (50)': ('Item', None, 0x55, None),
|
||||
'Rupees (200)': ('Item', None, 0x56, None),
|
||||
'Biggoron Sword': ('Item', True, 0x57, None),
|
||||
'Fire Arrows': ('Item', True, 0x58, None),
|
||||
'Ice Arrows': ('Item', True, 0x59, None),
|
||||
'Light Arrows': ('Item', True, 0x5A, None),
|
||||
'Gold Skulltula Token': ('Token', True, 0x5B, {'progressive': float('Inf')}),
|
||||
'Dins Fire': ('Item', True, 0x5C, None),
|
||||
'Nayrus Love': ('Item', True, 0x5E, None),
|
||||
'Farores Wind': ('Item', True, 0x5D, None),
|
||||
'Deku Nuts (10)': ('Item', None, 0x64, None),
|
||||
'Bombs (10)': ('Item', None, 0x66, None),
|
||||
'Bombs (20)': ('Item', None, 0x67, None),
|
||||
'Deku Seeds (30)': ('Item', None, 0x69, None),
|
||||
'Bombchus (5)': ('Item', True, 0x6A, None),
|
||||
'Bombchus (20)': ('Item', True, 0x6B, None),
|
||||
'Rupee (Treasure Chest Game)': ('Item', None, 0x72, None),
|
||||
'Piece of Heart (Treasure Chest Game)': ('Item', None, 0x76, None),
|
||||
'Ice Trap': ('Item', None, 0x7C, None),
|
||||
'Progressive Hookshot': ('Item', True, 0x80, {'progressive': 2}),
|
||||
'Progressive Strength Upgrade': ('Item', True, 0x81, {'progressive': 3}),
|
||||
'Bomb Bag': ('Item', True, 0x82, None),
|
||||
'Bow': ('Item', True, 0x83, None),
|
||||
'Slingshot': ('Item', True, 0x84, None),
|
||||
'Progressive Wallet': ('Item', True, 0x85, {'progressive': 3}),
|
||||
'Progressive Scale': ('Item', True, 0x86, {'progressive': 2}),
|
||||
'Deku Nut Capacity': ('Item', None, 0x87, None),
|
||||
'Deku Stick Capacity': ('Item', None, 0x88, None),
|
||||
'Bombchus': ('Item', True, 0x89, None),
|
||||
'Magic Meter': ('Item', True, 0x8A, None),
|
||||
'Ocarina': ('Item', True, 0x8B, None),
|
||||
'Bottle with Red Potion': ('Item', True, 0x8C, {'bottle': True, 'shop_object': 0x0F}),
|
||||
'Bottle with Green Potion': ('Item', True, 0x8D, {'bottle': True, 'shop_object': 0x0F}),
|
||||
'Bottle with Blue Potion': ('Item', True, 0x8E, {'bottle': True, 'shop_object': 0x0F}),
|
||||
'Bottle with Fairy': ('Item', True, 0x8F, {'bottle': True, 'shop_object': 0x0F}),
|
||||
'Bottle with Fish': ('Item', True, 0x90, {'bottle': True, 'shop_object': 0x0F}),
|
||||
'Bottle with Blue Fire': ('Item', True, 0x91, {'bottle': True, 'shop_object': 0x0F}),
|
||||
'Bottle with Bugs': ('Item', True, 0x92, {'bottle': True, 'shop_object': 0x0F}),
|
||||
'Bottle with Big Poe': ('Item', True, 0x93, {'shop_object': 0x0F}),
|
||||
'Bottle with Poe': ('Item', True, 0x94, {'bottle': True, 'shop_object': 0x0F}),
|
||||
'Boss Key (Forest Temple)': ('BossKey', True, 0x95, None),
|
||||
'Boss Key (Fire Temple)': ('BossKey', True, 0x96, None),
|
||||
'Boss Key (Water Temple)': ('BossKey', True, 0x97, None),
|
||||
'Boss Key (Spirit Temple)': ('BossKey', True, 0x98, None),
|
||||
'Boss Key (Shadow Temple)': ('BossKey', True, 0x99, None),
|
||||
'Boss Key (Ganons Castle)': ('GanonBossKey',True,0x9A,None),
|
||||
'Compass (Deku Tree)': ('Compass', None, 0x9B, None),
|
||||
'Compass (Dodongos Cavern)': ('Compass', None, 0x9C, None),
|
||||
'Compass (Jabu Jabus Belly)': ('Compass', None, 0x9D, None),
|
||||
'Compass (Forest Temple)': ('Compass', None, 0x9E, None),
|
||||
'Compass (Fire Temple)': ('Compass', None, 0x9F, None),
|
||||
'Compass (Water Temple)': ('Compass', None, 0xA0, None),
|
||||
'Compass (Spirit Temple)': ('Compass', None, 0xA1, None),
|
||||
'Compass (Shadow Temple)': ('Compass', None, 0xA2, None),
|
||||
'Compass (Bottom of the Well)': ('Compass', None, 0xA3, None),
|
||||
'Compass (Ice Cavern)': ('Compass', None, 0xA4, None),
|
||||
'Map (Deku Tree)': ('Map', None, 0xA5, None),
|
||||
'Map (Dodongos Cavern)': ('Map', None, 0xA6, None),
|
||||
'Map (Jabu Jabus Belly)': ('Map', None, 0xA7, None),
|
||||
'Map (Forest Temple)': ('Map', None, 0xA8, None),
|
||||
'Map (Fire Temple)': ('Map', None, 0xA9, None),
|
||||
'Map (Water Temple)': ('Map', None, 0xAA, None),
|
||||
'Map (Spirit Temple)': ('Map', None, 0xAB, None),
|
||||
'Map (Shadow Temple)': ('Map', None, 0xAC, None),
|
||||
'Map (Bottom of the Well)': ('Map', None, 0xAD, None),
|
||||
'Map (Ice Cavern)': ('Map', None, 0xAE, None),
|
||||
'Small Key (Forest Temple)': ('SmallKey', True, 0xAF, {'progressive': float('Inf')}),
|
||||
'Small Key (Fire Temple)': ('SmallKey', True, 0xB0, {'progressive': float('Inf')}),
|
||||
'Small Key (Water Temple)': ('SmallKey', True, 0xB1, {'progressive': float('Inf')}),
|
||||
'Small Key (Spirit Temple)': ('SmallKey', True, 0xB2, {'progressive': float('Inf')}),
|
||||
'Small Key (Shadow Temple)': ('SmallKey', True, 0xB3, {'progressive': float('Inf')}),
|
||||
'Small Key (Bottom of the Well)': ('SmallKey', True, 0xB4, {'progressive': float('Inf')}),
|
||||
'Small Key (Gerudo Training Grounds)': ('SmallKey',True, 0xB5, {'progressive': float('Inf')}),
|
||||
'Small Key (Gerudo Fortress)': ('FortressSmallKey',True, 0xB6, {'progressive': float('Inf')}),
|
||||
'Small Key (Ganons Castle)': ('SmallKey', True, 0xB7, {'progressive': float('Inf')}),
|
||||
'Double Defense': ('Item', True, 0xB8, None),
|
||||
'Magic Bean Pack': ('Item', True, 0xC9, None),
|
||||
'Triforce Piece': ('Item', True, 0xCA, {'progressive': float('Inf')}),
|
||||
'Zeldas Letter': ('Item', True, 0x0B, None),
|
||||
'Time Travel': ('Event', True, None, None),
|
||||
'Scarecrow Song': ('Event', True, None, None),
|
||||
'Triforce': ('Event', True, None, None),
|
||||
|
||||
# Event items otherwise generated by generic event logic
|
||||
# can be defined here to enforce their appearance in playthroughs.
|
||||
'Water Temple Clear': ('Event', True, None, None),
|
||||
'Forest Trial Clear': ('Event', True, None, None),
|
||||
'Fire Trial Clear': ('Event', True, None, None),
|
||||
'Water Trial Clear': ('Event', True, None, None),
|
||||
'Shadow Trial Clear': ('Event', True, None, None),
|
||||
'Spirit Trial Clear': ('Event', True, None, None),
|
||||
'Light Trial Clear': ('Event', True, None, None),
|
||||
|
||||
'Deku Stick Drop': ('Drop', True, None, None),
|
||||
'Deku Nut Drop': ('Drop', True, None, None),
|
||||
'Blue Fire': ('Drop', True, None, None),
|
||||
'Fairy': ('Drop', True, None, None),
|
||||
'Fish': ('Drop', True, None, None),
|
||||
'Bugs': ('Drop', True, None, None),
|
||||
'Big Poe': ('Drop', True, None, None),
|
||||
'Bombchu Drop': ('Drop', True, None, None),
|
||||
|
||||
# Consumable refills defined mostly to placate 'starting with' options
|
||||
'Arrows': ('Refill', None, None, None),
|
||||
'Bombs': ('Refill', None, None, None),
|
||||
'Deku Seeds': ('Refill', None, None, None),
|
||||
'Deku Sticks': ('Refill', None, None, None),
|
||||
'Deku Nuts': ('Refill', None, None, None),
|
||||
'Rupees': ('Refill', None, None, None),
|
||||
|
||||
'Minuet of Forest': ('Song', True, 0xBB,
|
||||
{
|
||||
'text_id': 0x73,
|
||||
'song_id': 0x02,
|
||||
'item_id': 0x5A,
|
||||
}),
|
||||
'Bolero of Fire': ('Song', True, 0xBC,
|
||||
{
|
||||
'text_id': 0x74,
|
||||
'song_id': 0x03,
|
||||
'item_id': 0x5B,
|
||||
}),
|
||||
'Serenade of Water': ('Song', True, 0xBD,
|
||||
{
|
||||
'text_id': 0x75,
|
||||
'song_id': 0x04,
|
||||
'item_id': 0x5C,
|
||||
}),
|
||||
'Requiem of Spirit': ('Song', True, 0xBE,
|
||||
{
|
||||
'text_id': 0x76,
|
||||
'song_id': 0x05,
|
||||
'item_id': 0x5D,
|
||||
}),
|
||||
'Nocturne of Shadow': ('Song', True, 0xBF,
|
||||
{
|
||||
'text_id': 0x77,
|
||||
'song_id': 0x06,
|
||||
'item_id': 0x5E,
|
||||
}),
|
||||
'Prelude of Light': ('Song', True, 0xC0,
|
||||
{
|
||||
'text_id': 0x78,
|
||||
'song_id': 0x07,
|
||||
'item_id': 0x5F,
|
||||
}),
|
||||
'Zeldas Lullaby': ('Song', True, 0xC1,
|
||||
{
|
||||
'text_id': 0xD4,
|
||||
'song_id': 0x0A,
|
||||
'item_id': 0x60,
|
||||
}),
|
||||
'Eponas Song': ('Song', True, 0xC2,
|
||||
{
|
||||
'text_id': 0xD2,
|
||||
'song_id': 0x09,
|
||||
'item_id': 0x61,
|
||||
}),
|
||||
'Sarias Song': ('Song', True, 0xC3,
|
||||
{
|
||||
'text_id': 0xD1,
|
||||
'song_id': 0x08,
|
||||
'item_id': 0x62,
|
||||
}),
|
||||
'Suns Song': ('Song', True, 0xC4,
|
||||
{
|
||||
'text_id': 0xD3,
|
||||
'song_id': 0x0B,
|
||||
'item_id': 0x63,
|
||||
}),
|
||||
'Song of Time': ('Song', True, 0xC5,
|
||||
{
|
||||
'text_id': 0xD5,
|
||||
'song_id': 0x0C,
|
||||
'item_id': 0x64,
|
||||
}),
|
||||
'Song of Storms': ('Song', True, 0xC6,
|
||||
{
|
||||
'text_id': 0xD6,
|
||||
'song_id': 0x0D,
|
||||
'item_id': 0x65,
|
||||
}),
|
||||
|
||||
'Buy Deku Nut (5)': ('Shop', True, 0x00, {'object': 0x00BB, 'price': 15}),
|
||||
'Buy Arrows (30)': ('Shop', False, 0x01, {'object': 0x00D8, 'price': 60}),
|
||||
'Buy Arrows (50)': ('Shop', False, 0x02, {'object': 0x00D8, 'price': 90}),
|
||||
'Buy Bombs (5) [25]': ('Shop', False, 0x03, {'object': 0x00CE, 'price': 25}),
|
||||
'Buy Deku Nut (10)': ('Shop', True, 0x04, {'object': 0x00BB, 'price': 30}),
|
||||
'Buy Deku Stick (1)': ('Shop', True, 0x05, {'object': 0x00C7, 'price': 10}),
|
||||
'Buy Bombs (10)': ('Shop', False, 0x06, {'object': 0x00CE, 'price': 50}),
|
||||
'Buy Fish': ('Shop', True, 0x07, {'object': 0x00F4, 'price': 200}),
|
||||
'Buy Red Potion [30]': ('Shop', False, 0x08, {'object': 0x00EB, 'price': 30}),
|
||||
'Buy Green Potion': ('Shop', False, 0x09, {'object': 0x00EB, 'price': 30}),
|
||||
'Buy Blue Potion': ('Shop', False, 0x0A, {'object': 0x00EB, 'price': 100}),
|
||||
'Buy Hylian Shield': ('Shop', True, 0x0C, {'object': 0x00DC, 'price': 80}),
|
||||
'Buy Deku Shield': ('Shop', True, 0x0D, {'object': 0x00CB, 'price': 40}),
|
||||
'Buy Goron Tunic': ('Shop', True, 0x0E, {'object': 0x00F2, 'price': 200}),
|
||||
'Buy Zora Tunic': ('Shop', True, 0x0F, {'object': 0x00F2, 'price': 300}),
|
||||
'Buy Heart': ('Shop', False, 0x10, {'object': 0x00B7, 'price': 10}),
|
||||
'Buy Bombchu (10)': ('Shop', True, 0x15, {'object': 0x00D9, 'price': 99}),
|
||||
'Buy Bombchu (20)': ('Shop', True, 0x16, {'object': 0x00D9, 'price': 180}),
|
||||
'Buy Bombchu (5)': ('Shop', True, 0x18, {'object': 0x00D9, 'price': 60}),
|
||||
'Buy Deku Seeds (30)': ('Shop', False, 0x1D, {'object': 0x0119, 'price': 30}),
|
||||
'Sold Out': ('Shop', False, 0x26, {'object': 0x0148}),
|
||||
'Buy Blue Fire': ('Shop', True, 0x27, {'object': 0x0173, 'price': 300}),
|
||||
'Buy Bottle Bug': ('Shop', True, 0x28, {'object': 0x0174, 'price': 50}),
|
||||
'Buy Poe': ('Shop', False, 0x2A, {'object': 0x0176, 'price': 30}),
|
||||
'Buy Fairy\'s Spirit': ('Shop', True, 0x2B, {'object': 0x0177, 'price': 50}),
|
||||
'Buy Arrows (10)': ('Shop', False, 0x2C, {'object': 0x00D8, 'price': 20}),
|
||||
'Buy Bombs (20)': ('Shop', False, 0x2D, {'object': 0x00CE, 'price': 80}),
|
||||
'Buy Bombs (30)': ('Shop', False, 0x2E, {'object': 0x00CE, 'price': 120}),
|
||||
'Buy Bombs (5) [35]': ('Shop', False, 0x2F, {'object': 0x00CE, 'price': 35}),
|
||||
'Buy Red Potion [40]': ('Shop', False, 0x30, {'object': 0x00EB, 'price': 40}),
|
||||
'Buy Red Potion [50]': ('Shop', False, 0x31, {'object': 0x00EB, 'price': 50}),
|
||||
|
||||
'Kokiri Emerald': ('DungeonReward', True, None,
|
||||
{
|
||||
'stone': True,
|
||||
'addr2_data': 0x80,
|
||||
'bit_mask': 0x00040000,
|
||||
'item_id': 0x6C,
|
||||
'actor_type': 0x13,
|
||||
'object_id': 0x00AD,
|
||||
}),
|
||||
'Goron Ruby': ('DungeonReward', True, None,
|
||||
{
|
||||
'stone': True,
|
||||
'addr2_data': 0x81,
|
||||
'bit_mask': 0x00080000,
|
||||
'item_id': 0x6D,
|
||||
'actor_type': 0x14,
|
||||
'object_id': 0x00AD,
|
||||
}),
|
||||
'Zora Sapphire': ('DungeonReward', True, None,
|
||||
{
|
||||
'stone': True,
|
||||
'addr2_data': 0x82,
|
||||
'bit_mask': 0x00100000,
|
||||
'item_id': 0x6E,
|
||||
'actor_type': 0x15,
|
||||
'object_id': 0x00AD,
|
||||
}),
|
||||
'Forest Medallion': ('DungeonReward', True, None,
|
||||
{
|
||||
'medallion': True,
|
||||
'addr2_data': 0x3E,
|
||||
'bit_mask': 0x00000001,
|
||||
'item_id': 0x66,
|
||||
'actor_type': 0x0B,
|
||||
'object_id': 0x00BA,
|
||||
}),
|
||||
'Fire Medallion': ('DungeonReward', True, None,
|
||||
{
|
||||
'medallion': True,
|
||||
'addr2_data': 0x3C,
|
||||
'bit_mask': 0x00000002,
|
||||
'item_id': 0x67,
|
||||
'actor_type': 0x09,
|
||||
'object_id': 0x00BA,
|
||||
}),
|
||||
'Water Medallion': ('DungeonReward', True, None,
|
||||
{
|
||||
'medallion': True,
|
||||
'addr2_data': 0x3D,
|
||||
'bit_mask': 0x00000004,
|
||||
'item_id': 0x68,
|
||||
'actor_type': 0x0A,
|
||||
'object_id': 0x00BA,
|
||||
}),
|
||||
'Spirit Medallion': ('DungeonReward', True, None,
|
||||
{
|
||||
'medallion': True,
|
||||
'addr2_data': 0x3F,
|
||||
'bit_mask': 0x00000008,
|
||||
'item_id': 0x69,
|
||||
'actor_type': 0x0C,
|
||||
'object_id': 0x00BA,
|
||||
}),
|
||||
'Shadow Medallion': ('DungeonReward', True, None,
|
||||
{
|
||||
'medallion': True,
|
||||
'addr2_data': 0x41,
|
||||
'bit_mask': 0x00000010,
|
||||
'item_id': 0x6A,
|
||||
'actor_type': 0x0D,
|
||||
'object_id': 0x00BA,
|
||||
}),
|
||||
'Light Medallion': ('DungeonReward', True, None,
|
||||
{
|
||||
'medallion': True,
|
||||
'addr2_data': 0x40,
|
||||
'bit_mask': 0x00000020,
|
||||
'item_id': 0x6B,
|
||||
'actor_type': 0x0E,
|
||||
'object_id': 0x00BA,
|
||||
}),
|
||||
}
|
||||
122
worlds/oot/JSONDump.py
Normal file
122
worlds/oot/JSONDump.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import json
|
||||
|
||||
from functools import reduce
|
||||
|
||||
INDENT = ' '
|
||||
|
||||
class CollapseList(list):
|
||||
pass
|
||||
class CollapseDict(dict):
|
||||
pass
|
||||
class AlignedDict(dict):
|
||||
def __init__(self, src_dict, depth):
|
||||
self.depth = depth - 1
|
||||
super().__init__(src_dict)
|
||||
class SortedDict(dict):
|
||||
pass
|
||||
|
||||
|
||||
def is_scalar(value):
|
||||
return not is_list(value) and not is_dict(value)
|
||||
|
||||
|
||||
def is_list(value):
|
||||
return isinstance(value, list) or isinstance(value, tuple)
|
||||
|
||||
|
||||
def is_dict(value):
|
||||
return isinstance(value, dict)
|
||||
|
||||
|
||||
def dump_scalar(obj, ensure_ascii=False):
|
||||
return json.dumps(obj, ensure_ascii=ensure_ascii)
|
||||
|
||||
|
||||
def dump_list(obj, current_indent='', ensure_ascii=False):
|
||||
entries = [dump_obj(value, current_indent + INDENT, ensure_ascii=ensure_ascii) for value in obj]
|
||||
|
||||
if len(entries) == 0:
|
||||
return '[]'
|
||||
|
||||
if isinstance(obj, CollapseList):
|
||||
values_format = '{value}'
|
||||
output_format = '[{values}]'
|
||||
join_format = ', '
|
||||
else:
|
||||
values_format = '{indent}{value}'
|
||||
output_format = '[\n{values}\n{indent}]'
|
||||
join_format = ',\n'
|
||||
|
||||
output = output_format.format(
|
||||
indent=current_indent,
|
||||
values=join_format.join([values_format.format(
|
||||
value=entry,
|
||||
indent=current_indent + INDENT
|
||||
) for entry in entries])
|
||||
)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def get_keys(obj, depth):
|
||||
if depth == 0:
|
||||
yield from obj.keys()
|
||||
else:
|
||||
for value in obj.values():
|
||||
yield from get_keys(value, depth - 1)
|
||||
|
||||
|
||||
def dump_dict(obj, current_indent='', sub_width=None, ensure_ascii=False):
|
||||
entries = []
|
||||
|
||||
key_width = None
|
||||
if sub_width is not None:
|
||||
sub_width = (sub_width[0]-1, sub_width[1])
|
||||
if sub_width[0] == 0:
|
||||
key_width = sub_width[1]
|
||||
|
||||
if isinstance(obj, AlignedDict):
|
||||
sub_keys = get_keys(obj, obj.depth)
|
||||
sub_width = (obj.depth, reduce(lambda acc, entry: max(acc, len(entry)), sub_keys, 0))
|
||||
|
||||
for key, value in obj.items():
|
||||
entries.append((dump_scalar(str(key), ensure_ascii), dump_obj(value, current_indent + INDENT, sub_width, ensure_ascii)))
|
||||
|
||||
if key_width is None:
|
||||
key_width = reduce(lambda acc, entry: max(acc, len(entry[0])), entries, 0)
|
||||
|
||||
if len(entries) == 0:
|
||||
return '{}'
|
||||
|
||||
if isinstance(obj, SortedDict):
|
||||
entries.sort(key=lambda item: item[0])
|
||||
|
||||
if isinstance(obj, CollapseDict):
|
||||
values_format = '{key} {value}'
|
||||
output_format = '{{{values}}}'
|
||||
join_format = ', '
|
||||
else:
|
||||
values_format = '{indent}{key:{padding}}{value}'
|
||||
output_format = '{{\n{values}\n{indent}}}'
|
||||
join_format = ',\n'
|
||||
|
||||
output = output_format.format(
|
||||
indent=current_indent,
|
||||
values=join_format.join([values_format.format(
|
||||
key='{key}:'.format(key=key),
|
||||
value=value,
|
||||
indent=current_indent + INDENT,
|
||||
padding=key_width + 2,
|
||||
) for (key, value) in entries])
|
||||
)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def dump_obj(obj, current_indent='', sub_width=None, ensure_ascii=False):
|
||||
if is_list(obj):
|
||||
return dump_list(obj, current_indent, ensure_ascii)
|
||||
elif is_dict(obj):
|
||||
return dump_dict(obj, current_indent, sub_width, ensure_ascii)
|
||||
else:
|
||||
return dump_scalar(obj, ensure_ascii)
|
||||
26
worlds/oot/LICENSE
Normal file
26
worlds/oot/LICENSE
Normal file
@@ -0,0 +1,26 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Amazing Ampharos
|
||||
Copyright (c) 2021 espeon65536
|
||||
|
||||
Credit for contributions to Junglechief87 on this and to LLCoolDave and
|
||||
KevinCathcart for their work on the Zelda Lttp Entrance Randomizer which
|
||||
was the code base for this project.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
53
worlds/oot/Location.py
Normal file
53
worlds/oot/Location.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from .LocationList import location_table
|
||||
from BaseClasses import Location
|
||||
|
||||
location_id_offset = 67000
|
||||
location_name_to_id = {name: (location_id_offset + index) for (index, name) in enumerate(location_table)
|
||||
if location_table[name][0] not in ['Boss', 'Event', 'Drop', 'HintStone', 'Hint']}
|
||||
|
||||
class OOTLocation(Location):
|
||||
game: str = 'Ocarina of Time'
|
||||
|
||||
def __init__(self, player, name='', code=None, address1=None, address2=None, default=None, type='Chest', scene=None, parent=None, filter_tags=None, internal=False):
|
||||
super(OOTLocation, self).__init__(player, name, code, parent)
|
||||
self.address1 = address1
|
||||
self.address2 = address2
|
||||
self.default = default
|
||||
self.type = type
|
||||
self.scene = scene
|
||||
self.internal = internal
|
||||
if filter_tags is None:
|
||||
self.filter_tags = None
|
||||
else:
|
||||
self.filter_tags = list(filter_tags)
|
||||
self.never = False # no idea what this does
|
||||
|
||||
if type == 'Event':
|
||||
self.event = True
|
||||
|
||||
|
||||
def LocationFactory(locations, player: int):
|
||||
ret = []
|
||||
singleton = False
|
||||
if isinstance(locations, str):
|
||||
locations = [locations]
|
||||
singleton = True
|
||||
for location in locations:
|
||||
if location in location_table:
|
||||
match_location = location
|
||||
else:
|
||||
match_location = next(filter(lambda k: k.lower() == location.lower(), location_table), None)
|
||||
if match_location:
|
||||
type, scene, default, addresses, vanilla_item, filter_tags = location_table[match_location]
|
||||
if addresses is None:
|
||||
addresses = (None, None)
|
||||
address1, address2 = addresses
|
||||
ret.append(OOTLocation(player, match_location, location_name_to_id.get(match_location, None), address1, address2, default, type, scene, filter_tags=filter_tags))
|
||||
else:
|
||||
raise KeyError('Unknown Location: %s', location)
|
||||
|
||||
if singleton:
|
||||
return ret[0]
|
||||
return ret
|
||||
|
||||
|
||||
932
worlds/oot/LocationList.py
Normal file
932
worlds/oot/LocationList.py
Normal file
@@ -0,0 +1,932 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
def shop_address(shop_id, shelf_id):
|
||||
return 0xC71ED0 + (0x40 * shop_id) + (0x08 * shelf_id)
|
||||
|
||||
# Abbreviations
|
||||
# DMC Death Mountain Crater
|
||||
# DMT Death Mountain Trail
|
||||
# GC Goron City
|
||||
# GF Gerudo Fortress
|
||||
# GS Gold Skulltula
|
||||
# GV Gerudo Valley
|
||||
# HC Hyrule Castle
|
||||
# HF Hyrule Field
|
||||
# KF Kokiri Forest
|
||||
# LH Lake Hylia
|
||||
# LLR Lon Lon Ranch
|
||||
# LW Lost Woods
|
||||
# OGC Outside Ganon's Castle
|
||||
# SFM Sacred Forest Meadow
|
||||
# ToT Temple of Time
|
||||
# ZD Zora's Domain
|
||||
# ZF Zora's Fountain
|
||||
# ZR Zora's River
|
||||
|
||||
# The order of this table is reflected in the spoiler's list of locations (except Hints aren't included).
|
||||
# Within a section, the order of types is: gifts/freestanding/chests, Deku Scrubs, Cows, Gold Skulltulas, Shops.
|
||||
|
||||
# NPC Scrubs are on the overworld, while GrottoNPC is a special handler for Grottos
|
||||
# Grottos scrubs are the same scene and actor, so we use a unique grotto ID for the scene
|
||||
|
||||
# Note that the scene for skulltulas is not the actual scene the token appears in
|
||||
# Rather, it is the index of the grouping used when storing skulltula collection
|
||||
# For example, zora river, zora's domain, and zora fountain are all a single 'scene' for skulltulas
|
||||
|
||||
# Location: Type Scene Default Addresses Vanilla Item Categories
|
||||
location_table = OrderedDict([
|
||||
## Dungeon Rewards
|
||||
("Links Pocket", ("Boss", None, None, None, 'Light Medallion', None)),
|
||||
("Queen Gohma", ("Boss", None, 0x6C, (0x0CA315F, 0x2079571), 'Kokiri Emerald', None)),
|
||||
("King Dodongo", ("Boss", None, 0x6D, (0x0CA30DF, 0x2223309), 'Goron Ruby', None)),
|
||||
("Barinade", ("Boss", None, 0x6E, (0x0CA36EB, 0x2113C19), 'Zora Sapphire', None)),
|
||||
("Phantom Ganon", ("Boss", None, 0x66, (0x0CA3D07, 0x0D4ED79), 'Forest Medallion', None)),
|
||||
("Volvagia", ("Boss", None, 0x67, (0x0CA3D93, 0x0D10135), 'Fire Medallion', None)),
|
||||
("Morpha", ("Boss", None, 0x68, (0x0CA3E1F, 0x0D5A3A9), 'Water Medallion', None)),
|
||||
("Bongo Bongo", ("Boss", None, 0x6A, (0x0CA3F43, 0x0D13E19), 'Shadow Medallion', None)),
|
||||
("Twinrova", ("Boss", None, 0x69, (0x0CA3EB3, 0x0D39FF1), 'Spirit Medallion', None)),
|
||||
("Ganon", ("Event", None, None, None, 'Triforce', None)),
|
||||
|
||||
## Songs
|
||||
("Song from Impa", ("Song", 0xFF, 0x26, (0x2E8E925, 0x2E8E925), 'Zeldas Lullaby', ("Hyrule Castle", "Market", "Songs"))),
|
||||
("Song from Malon", ("Song", 0xFF, 0x27, (0x0D7EB53, 0x0D7EBCF), 'Eponas Song', ("Lon Lon Ranch", "Songs",))),
|
||||
("Song from Saria", ("Song", 0xFF, 0x28, (0x20B1DB1, 0x20B1DB1), 'Sarias Song', ("Sacred Forest Meadow", "Forest", "Songs"))),
|
||||
("Song from Composers Grave", ("Song", 0xFF, 0x29, (0x332A871, 0x332A871), 'Suns Song', ("the Graveyard", "Kakariko", "Songs"))),
|
||||
("Song from Ocarina of Time", ("Song", 0xFF, 0x2A, (0x252FC89, 0x252FC89), 'Song of Time', ("Hyrule Field", "Songs", "Need Spiritual Stones"))),
|
||||
("Song from Windmill", ("Song", 0xFF, 0x2B, (0x0E42C07, 0x0E42B8B), 'Song of Storms', ("Kakariko Village", "Kakariko", "Songs"))),
|
||||
("Sheik in Forest", ("Song", 0xFF, 0x20, (0x20B0809, 0x20B0809), 'Minuet of Forest', ("Sacred Forest Meadow", "Forest", "Songs"))),
|
||||
("Sheik in Crater", ("Song", 0xFF, 0x21, (0x224D7F1, 0x224D7F1), 'Bolero of Fire', ("Death Mountain Crater", "Death Mountain", "Songs"))),
|
||||
("Sheik in Ice Cavern", ("Song", 0xFF, 0x22, (0x2BEC889, 0x2BEC889), 'Serenade of Water', ("Ice Cavern", "Songs",))),
|
||||
("Sheik at Colossus", ("Song", 0xFF, 0x23, (0x218C57D, 0x218C57D), 'Requiem of Spirit', ("Desert Colossus", "Songs",))),
|
||||
("Sheik in Kakariko", ("Song", 0xFF, 0x24, (0x2000FE1, 0x2000FE1), 'Nocturne of Shadow', ("Kakariko Village", "Kakariko", "Songs"))),
|
||||
("Sheik at Temple", ("Song", 0xFF, 0x25, (0x2531329, 0x2531329), 'Prelude of Light', ("Temple of Time", "Market", "Songs"))),
|
||||
|
||||
## Overworld
|
||||
# Kokiri Forest
|
||||
("KF Midos Top Left Chest", ("Chest", 0x28, 0x00, None, 'Rupees (5)', ("Kokiri Forest", "Forest",))),
|
||||
("KF Midos Top Right Chest", ("Chest", 0x28, 0x01, None, 'Rupees (5)', ("Kokiri Forest", "Forest",))),
|
||||
("KF Midos Bottom Left Chest", ("Chest", 0x28, 0x02, None, 'Rupee (1)', ("Kokiri Forest", "Forest",))),
|
||||
("KF Midos Bottom Right Chest", ("Chest", 0x28, 0x03, None, 'Recovery Heart', ("Kokiri Forest", "Forest",))),
|
||||
("KF Kokiri Sword Chest", ("Chest", 0x55, 0x00, None, 'Kokiri Sword', ("Kokiri Forest", "Forest",))),
|
||||
("KF Storms Grotto Chest", ("Chest", 0x3E, 0x0C, None, 'Rupees (20)', ("Kokiri Forest", "Forest", "Grottos"))),
|
||||
("KF Links House Cow", ("NPC", 0x34, 0x15, None, 'Milk', ("KF Links House", "Forest", "Cow", "Minigames"))),
|
||||
("KF GS Know It All House", ("GS Token", 0x0C, 0x02, None, 'Gold Skulltula Token', ("Kokiri Forest", "Skulltulas",))),
|
||||
("KF GS Bean Patch", ("GS Token", 0x0C, 0x01, None, 'Gold Skulltula Token', ("Kokiri Forest", "Skulltulas",))),
|
||||
("KF GS House of Twins", ("GS Token", 0x0C, 0x04, None, 'Gold Skulltula Token', ("Kokiri Forest", "Skulltulas",))),
|
||||
("KF Shop Item 1", ("Shop", 0x2D, 0x30, (shop_address(0, 0), None), 'Buy Deku Shield', ("Kokiri Forest", "Forest", "Shops"))),
|
||||
("KF Shop Item 2", ("Shop", 0x2D, 0x31, (shop_address(0, 1), None), 'Buy Deku Nut (5)', ("Kokiri Forest", "Forest", "Shops"))),
|
||||
("KF Shop Item 3", ("Shop", 0x2D, 0x32, (shop_address(0, 2), None), 'Buy Deku Nut (10)', ("Kokiri Forest", "Forest", "Shops"))),
|
||||
("KF Shop Item 4", ("Shop", 0x2D, 0x33, (shop_address(0, 3), None), 'Buy Deku Stick (1)', ("Kokiri Forest", "Forest", "Shops"))),
|
||||
("KF Shop Item 5", ("Shop", 0x2D, 0x34, (shop_address(0, 4), None), 'Buy Deku Seeds (30)', ("Kokiri Forest", "Forest", "Shops"))),
|
||||
("KF Shop Item 6", ("Shop", 0x2D, 0x35, (shop_address(0, 5), None), 'Buy Arrows (10)', ("Kokiri Forest", "Forest", "Shops"))),
|
||||
("KF Shop Item 7", ("Shop", 0x2D, 0x36, (shop_address(0, 6), None), 'Buy Arrows (30)', ("Kokiri Forest", "Forest", "Shops"))),
|
||||
("KF Shop Item 8", ("Shop", 0x2D, 0x37, (shop_address(0, 7), None), 'Buy Heart', ("Kokiri Forest", "Forest", "Shops"))),
|
||||
|
||||
# Lost Woods
|
||||
("LW Gift from Saria", ("Cutscene", 0xFF, 0x02, None, 'Ocarina', ("the Lost Woods", "Forest",))),
|
||||
("LW Ocarina Memory Game", ("NPC", 0x5B, 0x76, None, 'Piece of Heart', ("the Lost Woods", "Forest", "Minigames"))),
|
||||
("LW Target in Woods", ("NPC", 0x5B, 0x60, None, 'Slingshot', ("the Lost Woods", "Forest",))),
|
||||
("LW Near Shortcuts Grotto Chest", ("Chest", 0x3E, 0x14, None, 'Rupees (5)', ("the Lost Woods", "Forest", "Grottos"))),
|
||||
("Deku Theater Skull Mask", ("NPC", 0x3E, 0x77, None, 'Deku Stick Capacity', ("the Lost Woods", "Forest", "Grottos"))),
|
||||
("Deku Theater Mask of Truth", ("NPC", 0x3E, 0x7A, None, 'Deku Nut Capacity', ("the Lost Woods", "Forest", "Need Spiritual Stones", "Grottos"))),
|
||||
("LW Skull Kid", ("NPC", 0x5B, 0x3E, None, 'Piece of Heart', ("the Lost Woods", "Forest",))),
|
||||
("LW Deku Scrub Near Bridge", ("NPC", 0x5B, 0x77, None, 'Deku Stick Capacity', ("the Lost Woods", "Forest", "Deku Scrub", "Deku Scrub Upgrades"))),
|
||||
("LW Deku Scrub Near Deku Theater Left", ("NPC", 0x5B, 0x31, None, 'Buy Deku Stick (1)', ("the Lost Woods", "Forest", "Deku Scrub"))),
|
||||
("LW Deku Scrub Near Deku Theater Right", ("NPC", 0x5B, 0x30, None, 'Buy Deku Nut (5)', ("the Lost Woods", "Forest", "Deku Scrub"))),
|
||||
("LW Deku Scrub Grotto Front", ("GrottoNPC", 0xF5, 0x79, None, 'Deku Nut Capacity', ("the Lost Woods", "Forest", "Deku Scrub", "Deku Scrub Upgrades", "Grottos"))),
|
||||
("LW Deku Scrub Grotto Rear", ("GrottoNPC", 0xF5, 0x33, None, 'Buy Deku Seeds (30)', ("the Lost Woods", "Forest", "Deku Scrub", "Grottos"))),
|
||||
("LW GS Bean Patch Near Bridge", ("GS Token", 0x0D, 0x01, None, 'Gold Skulltula Token', ("the Lost Woods", "Skulltulas",))),
|
||||
("LW GS Bean Patch Near Theater", ("GS Token", 0x0D, 0x02, None, 'Gold Skulltula Token', ("the Lost Woods", "Skulltulas",))),
|
||||
("LW GS Above Theater", ("GS Token", 0x0D, 0x04, None, 'Gold Skulltula Token', ("the Lost Woods", "Skulltulas",))),
|
||||
|
||||
# Sacred Forest Meadow
|
||||
("SFM Wolfos Grotto Chest", ("Chest", 0x3E, 0x11, None, 'Rupees (50)', ("Sacred Forest Meadow", "Forest", "Grottos"))),
|
||||
("SFM Deku Scrub Grotto Front", ("GrottoNPC", 0xEE, 0x3A, None, 'Buy Green Potion', ("Sacred Forest Meadow", "Forest", "Deku Scrub", "Grottos"))),
|
||||
("SFM Deku Scrub Grotto Rear", ("GrottoNPC", 0xEE, 0x39, None, 'Buy Red Potion [30]', ("Sacred Forest Meadow", "Forest", "Deku Scrub", "Grottos"))),
|
||||
("SFM GS", ("GS Token", 0x0D, 0x08, None, 'Gold Skulltula Token', ("Sacred Forest Meadow", "Skulltulas",))),
|
||||
|
||||
# Hyrule Field
|
||||
("HF Ocarina of Time Item", ("NPC", 0x51, 0x0C, None, 'Ocarina', ("Hyrule Field", "Need Spiritual Stones",))),
|
||||
("HF Near Market Grotto Chest", ("Chest", 0x3E, 0x00, None, 'Rupees (5)', ("Hyrule Field", "Grottos",))),
|
||||
("HF Tektite Grotto Freestanding PoH", ("Collectable", 0x3E, 0x01, None, 'Piece of Heart', ("Hyrule Field", "Grottos",))),
|
||||
("HF Southeast Grotto Chest", ("Chest", 0x3E, 0x02, None, 'Rupees (20)', ("Hyrule Field", "Grottos",))),
|
||||
("HF Open Grotto Chest", ("Chest", 0x3E, 0x03, None, 'Rupees (5)', ("Hyrule Field", "Grottos",))),
|
||||
("HF Deku Scrub Grotto", ("GrottoNPC", 0xE6, 0x3E, None, 'Piece of Heart', ("Hyrule Field", "Deku Scrub", "Deku Scrub Upgrades", "Grottos"))),
|
||||
("HF Cow Grotto Cow", ("NPC", 0x3E, 0x16, None, 'Milk', ("Hyrule Field", "Cow", "Grottos"))),
|
||||
("HF GS Cow Grotto", ("GS Token", 0x0A, 0x01, None, 'Gold Skulltula Token', ("Hyrule Field", "Skulltulas", "Grottos"))),
|
||||
("HF GS Near Kak Grotto", ("GS Token", 0x0A, 0x02, None, 'Gold Skulltula Token', ("Hyrule Field", "Skulltulas", "Grottos"))),
|
||||
|
||||
# Market
|
||||
("Market Shooting Gallery Reward", ("NPC", 0x42, 0x60, None, 'Slingshot', ("the Market", "Market", "Minigames"))),
|
||||
("Market Bombchu Bowling First Prize", ("NPC", 0x4B, 0x34, None, 'Bomb Bag', ("the Market", "Market", "Minigames"))),
|
||||
("Market Bombchu Bowling Second Prize", ("NPC", 0x4B, 0x3E, None, 'Piece of Heart', ("the Market", "Market", "Minigames"))),
|
||||
("Market Bombchu Bowling Bombchus", ("Event", 0x4B, None, None, 'Bombchu Drop', ("the Market", "Market", "Minigames"))),
|
||||
("Market Lost Dog", ("NPC", 0x35, 0x3E, None, 'Piece of Heart', ("the Market", "Market",))),
|
||||
("Market Treasure Chest Game Reward", ("Chest", 0x10, 0x0A, None, 'Piece of Heart (Treasure Chest Game)', ("the Market", "Market", "Minigames"))),
|
||||
("Market 10 Big Poes", ("NPC", 0x4D, 0x0F, None, 'Bottle', ("the Market", "Hyrule Castle",))),
|
||||
("Market GS Guard House", ("GS Token", 0x0E, 0x08, None, 'Gold Skulltula Token', ("the Market", "Skulltulas",))),
|
||||
("Market Bazaar Item 1", ("Shop", 0x2C, 0x30, (shop_address(4, 0), None), 'Buy Hylian Shield', ("the Market", "Market", "Shops"))),
|
||||
("Market Bazaar Item 2", ("Shop", 0x2C, 0x31, (shop_address(4, 1), None), 'Buy Bombs (5) [35]', ("the Market", "Market", "Shops"))),
|
||||
("Market Bazaar Item 3", ("Shop", 0x2C, 0x32, (shop_address(4, 2), None), 'Buy Deku Nut (5)', ("the Market", "Market", "Shops"))),
|
||||
("Market Bazaar Item 4", ("Shop", 0x2C, 0x33, (shop_address(4, 3), None), 'Buy Heart', ("the Market", "Market", "Shops"))),
|
||||
("Market Bazaar Item 5", ("Shop", 0x2C, 0x34, (shop_address(4, 4), None), 'Buy Arrows (10)', ("the Market", "Market", "Shops"))),
|
||||
("Market Bazaar Item 6", ("Shop", 0x2C, 0x35, (shop_address(4, 5), None), 'Buy Arrows (50)', ("the Market", "Market", "Shops"))),
|
||||
("Market Bazaar Item 7", ("Shop", 0x2C, 0x36, (shop_address(4, 6), None), 'Buy Deku Stick (1)', ("the Market", "Market", "Shops"))),
|
||||
("Market Bazaar Item 8", ("Shop", 0x2C, 0x37, (shop_address(4, 7), None), 'Buy Arrows (30)', ("the Market", "Market", "Shops"))),
|
||||
|
||||
("Market Potion Shop Item 1", ("Shop", 0x31, 0x30, (shop_address(3, 0), None), 'Buy Green Potion', ("the Market", "Market", "Shops"))),
|
||||
("Market Potion Shop Item 2", ("Shop", 0x31, 0x31, (shop_address(3, 1), None), 'Buy Blue Fire', ("the Market", "Market", "Shops"))),
|
||||
("Market Potion Shop Item 3", ("Shop", 0x31, 0x32, (shop_address(3, 2), None), 'Buy Red Potion [30]', ("the Market", "Market", "Shops"))),
|
||||
("Market Potion Shop Item 4", ("Shop", 0x31, 0x33, (shop_address(3, 3), None), 'Buy Fairy\'s Spirit', ("the Market", "Market", "Shops"))),
|
||||
("Market Potion Shop Item 5", ("Shop", 0x31, 0x34, (shop_address(3, 4), None), 'Buy Deku Nut (5)', ("the Market", "Market", "Shops"))),
|
||||
("Market Potion Shop Item 6", ("Shop", 0x31, 0x35, (shop_address(3, 5), None), 'Buy Bottle Bug', ("the Market", "Market", "Shops"))),
|
||||
("Market Potion Shop Item 7", ("Shop", 0x31, 0x36, (shop_address(3, 6), None), 'Buy Poe', ("the Market", "Market", "Shops"))),
|
||||
("Market Potion Shop Item 8", ("Shop", 0x31, 0x37, (shop_address(3, 7), None), 'Buy Fish', ("the Market", "Market", "Shops"))),
|
||||
|
||||
("Market Bombchu Shop Item 1", ("Shop", 0x32, 0x30, (shop_address(2, 0), None), 'Buy Bombchu (5)', ("the Market", "Market", "Shops"))),
|
||||
("Market Bombchu Shop Item 2", ("Shop", 0x32, 0x31, (shop_address(2, 1), None), 'Buy Bombchu (10)', ("the Market", "Market", "Shops"))),
|
||||
("Market Bombchu Shop Item 3", ("Shop", 0x32, 0x32, (shop_address(2, 2), None), 'Buy Bombchu (10)', ("the Market", "Market", "Shops"))),
|
||||
("Market Bombchu Shop Item 4", ("Shop", 0x32, 0x33, (shop_address(2, 3), None), 'Buy Bombchu (10)', ("the Market", "Market", "Shops"))),
|
||||
("Market Bombchu Shop Item 5", ("Shop", 0x32, 0x34, (shop_address(2, 4), None), 'Buy Bombchu (20)', ("the Market", "Market", "Shops"))),
|
||||
("Market Bombchu Shop Item 6", ("Shop", 0x32, 0x35, (shop_address(2, 5), None), 'Buy Bombchu (20)', ("the Market", "Market", "Shops"))),
|
||||
("Market Bombchu Shop Item 7", ("Shop", 0x32, 0x36, (shop_address(2, 6), None), 'Buy Bombchu (20)', ("the Market", "Market", "Shops"))),
|
||||
("Market Bombchu Shop Item 8", ("Shop", 0x32, 0x37, (shop_address(2, 7), None), 'Buy Bombchu (20)', ("the Market", "Market", "Shops"))),
|
||||
|
||||
("ToT Light Arrows Cutscene", ("Cutscene", 0xFF, 0x01, None, 'Light Arrows', ("Temple of Time", "Market",))),
|
||||
|
||||
# Hyrule Castle
|
||||
("HC Malon Egg", ("NPC", 0x5F, 0x47, None, 'Weird Egg', ("Hyrule Castle", "Market",))),
|
||||
("HC Zeldas Letter", ("NPC", 0x4A, 0x0B, None, 'Zeldas Letter', ("Hyrule Castle", "Market",))),
|
||||
("HC Great Fairy Reward", ("Cutscene", 0xFF, 0x11, None, 'Dins Fire', ("Hyrule Castle", "Market", "Fairies"))),
|
||||
("HC GS Tree", ("GS Token", 0x0E, 0x04, None, 'Gold Skulltula Token', ("Hyrule Castle", "Skulltulas",))),
|
||||
("HC GS Storms Grotto", ("GS Token", 0x0E, 0x02, None, 'Gold Skulltula Token', ("Hyrule Castle", "Skulltulas", "Grottos"))),
|
||||
|
||||
# Lon Lon Ranch
|
||||
("LLR Talons Chickens", ("NPC", 0x4C, 0x14, None, 'Bottle with Milk', ("Lon Lon Ranch", "Kakariko", "Minigames"))),
|
||||
("LLR Freestanding PoH", ("Collectable", 0x4C, 0x01, None, 'Piece of Heart', ("Lon Lon Ranch",))),
|
||||
("LLR Deku Scrub Grotto Left", ("GrottoNPC", 0xFC, 0x30, None, 'Buy Deku Nut (5)', ("Lon Lon Ranch", "Deku Scrub", "Grottos"))),
|
||||
("LLR Deku Scrub Grotto Center", ("GrottoNPC", 0xFC, 0x33, None, 'Buy Deku Seeds (30)', ("Lon Lon Ranch", "Deku Scrub", "Grottos"))),
|
||||
("LLR Deku Scrub Grotto Right", ("GrottoNPC", 0xFC, 0x37, None, 'Buy Bombs (5) [35]', ("Lon Lon Ranch", "Deku Scrub", "Grottos"))),
|
||||
("LLR Stables Left Cow", ("NPC", 0x36, 0x15, None, 'Milk', ("Lon Lon Ranch", "Cow",))),
|
||||
("LLR Stables Right Cow", ("NPC", 0x36, 0x16, None, 'Milk', ("Lon Lon Ranch", "Cow",))),
|
||||
("LLR Tower Left Cow", ("NPC", 0x4C, 0x16, None, 'Milk', ("Lon Lon Ranch", "Cow",))),
|
||||
("LLR Tower Right Cow", ("NPC", 0x4C, 0x15, None, 'Milk', ("Lon Lon Ranch", "Cow",))),
|
||||
("LLR GS House Window", ("GS Token", 0x0B, 0x04, None, 'Gold Skulltula Token', ("Lon Lon Ranch", "Skulltulas",))),
|
||||
("LLR GS Tree", ("GS Token", 0x0B, 0x08, None, 'Gold Skulltula Token', ("Lon Lon Ranch", "Skulltulas",))),
|
||||
("LLR GS Rain Shed", ("GS Token", 0x0B, 0x02, None, 'Gold Skulltula Token', ("Lon Lon Ranch", "Skulltulas",))),
|
||||
("LLR GS Back Wall", ("GS Token", 0x0B, 0x01, None, 'Gold Skulltula Token', ("Lon Lon Ranch", "Skulltulas",))),
|
||||
|
||||
# Kakariko
|
||||
("Kak Anju as Child", ("NPC", 0x52, 0x0F, None, 'Bottle', ("Kakariko Village", "Kakariko", "Minigames"))),
|
||||
("Kak Anju as Adult", ("NPC", 0x52, 0x1D, None, 'Pocket Egg', ("Kakariko Village", "Kakariko",))),
|
||||
("Kak Impas House Freestanding PoH", ("Collectable", 0x37, 0x01, None, 'Piece of Heart', ("Kakariko Village", "Kakariko",))),
|
||||
("Kak Windmill Freestanding PoH", ("Collectable", 0x48, 0x01, None, 'Piece of Heart', ("Kakariko Village", "Kakariko",))),
|
||||
("Kak Man on Roof", ("NPC", 0x52, 0x3E, None, 'Piece of Heart', ("Kakariko Village", "Kakariko",))),
|
||||
("Kak Open Grotto Chest", ("Chest", 0x3E, 0x08, None, 'Rupees (20)', ("Kakariko Village", "Kakariko", "Grottos"))),
|
||||
("Kak Redead Grotto Chest", ("Chest", 0x3E, 0x0A, None, 'Rupees (200)', ("Kakariko Village", "Kakariko", "Grottos"))),
|
||||
("Kak Shooting Gallery Reward", ("NPC", 0x42, 0x30, None, 'Bow', ("Kakariko Village", "Kakariko", "Minigames"))),
|
||||
("Kak 10 Gold Skulltula Reward", ("NPC", 0x50, 0x45, None, 'Progressive Wallet', ("Kakariko Village", "Kakariko", "Skulltula House"))),
|
||||
("Kak 20 Gold Skulltula Reward", ("NPC", 0x50, 0x39, None, 'Stone of Agony', ("Kakariko Village", "Kakariko", "Skulltula House"))),
|
||||
("Kak 30 Gold Skulltula Reward", ("NPC", 0x50, 0x46, None, 'Progressive Wallet', ("Kakariko Village", "Kakariko", "Skulltula House"))),
|
||||
("Kak 40 Gold Skulltula Reward", ("NPC", 0x50, 0x03, None, 'Bombchus (10)', ("Kakariko Village", "Kakariko", "Skulltula House"))),
|
||||
("Kak 50 Gold Skulltula Reward", ("NPC", 0x50, 0x3E, None, 'Piece of Heart', ("Kakariko Village", "Kakariko", "Skulltula House"))),
|
||||
("Kak Impas House Cow", ("NPC", 0x37, 0x15, None, 'Milk', ("Kakariko Village", "Kakariko", "Cow"))),
|
||||
("Kak GS Tree", ("GS Token", 0x10, 0x20, None, 'Gold Skulltula Token', ("Kakariko Village", "Skulltulas",))),
|
||||
("Kak GS Guards House", ("GS Token", 0x10, 0x02, None, 'Gold Skulltula Token', ("Kakariko Village", "Skulltulas",))),
|
||||
("Kak GS Watchtower", ("GS Token", 0x10, 0x04, None, 'Gold Skulltula Token', ("Kakariko Village", "Skulltulas",))),
|
||||
("Kak GS Skulltula House", ("GS Token", 0x10, 0x10, None, 'Gold Skulltula Token', ("Kakariko Village", "Skulltulas",))),
|
||||
("Kak GS House Under Construction", ("GS Token", 0x10, 0x08, None, 'Gold Skulltula Token', ("Kakariko Village", "Skulltulas",))),
|
||||
("Kak GS Above Impas House", ("GS Token", 0x10, 0x40, None, 'Gold Skulltula Token', ("Kakariko Village", "Skulltulas",))),
|
||||
("Kak Bazaar Item 1", ("Shop", 0x2C, 0x38, (shop_address(5, 0), None), 'Buy Hylian Shield', ("Kakariko Village", "Kakariko", "Shops"))),
|
||||
("Kak Bazaar Item 2", ("Shop", 0x2C, 0x39, (shop_address(5, 1), None), 'Buy Bombs (5) [35]', ("Kakariko Village", "Kakariko", "Shops"))),
|
||||
("Kak Bazaar Item 3", ("Shop", 0x2C, 0x3A, (shop_address(5, 2), None), 'Buy Deku Nut (5)', ("Kakariko Village", "Kakariko", "Shops"))),
|
||||
("Kak Bazaar Item 4", ("Shop", 0x2C, 0x3B, (shop_address(5, 3), None), 'Buy Heart', ("Kakariko Village", "Kakariko", "Shops"))),
|
||||
("Kak Bazaar Item 5", ("Shop", 0x2C, 0x3D, (shop_address(5, 4), None), 'Buy Arrows (10)', ("Kakariko Village", "Kakariko", "Shops"))),
|
||||
("Kak Bazaar Item 6", ("Shop", 0x2C, 0x3E, (shop_address(5, 5), None), 'Buy Arrows (50)', ("Kakariko Village", "Kakariko", "Shops"))),
|
||||
("Kak Bazaar Item 7", ("Shop", 0x2C, 0x3F, (shop_address(5, 6), None), 'Buy Deku Stick (1)', ("Kakariko Village", "Kakariko", "Shops"))),
|
||||
("Kak Bazaar Item 8", ("Shop", 0x2C, 0x40, (shop_address(5, 7), None), 'Buy Arrows (30)', ("Kakariko Village", "Kakariko", "Shops"))),
|
||||
("Kak Potion Shop Item 1", ("Shop", 0x30, 0x30, (shop_address(1, 0), None), 'Buy Deku Nut (5)', ("Kakariko Village", "Kakariko", "Shops"))),
|
||||
("Kak Potion Shop Item 2", ("Shop", 0x30, 0x31, (shop_address(1, 1), None), 'Buy Fish', ("Kakariko Village", "Kakariko", "Shops"))),
|
||||
("Kak Potion Shop Item 3", ("Shop", 0x30, 0x32, (shop_address(1, 2), None), 'Buy Red Potion [30]', ("Kakariko Village", "Kakariko", "Shops"))),
|
||||
("Kak Potion Shop Item 4", ("Shop", 0x30, 0x33, (shop_address(1, 3), None), 'Buy Green Potion', ("Kakariko Village", "Kakariko", "Shops"))),
|
||||
("Kak Potion Shop Item 5", ("Shop", 0x30, 0x34, (shop_address(1, 4), None), 'Buy Blue Fire', ("Kakariko Village", "Kakariko", "Shops"))),
|
||||
("Kak Potion Shop Item 6", ("Shop", 0x30, 0x35, (shop_address(1, 5), None), 'Buy Bottle Bug', ("Kakariko Village", "Kakariko", "Shops"))),
|
||||
("Kak Potion Shop Item 7", ("Shop", 0x30, 0x36, (shop_address(1, 6), None), 'Buy Poe', ("Kakariko Village", "Kakariko", "Shops"))),
|
||||
("Kak Potion Shop Item 8", ("Shop", 0x30, 0x37, (shop_address(1, 7), None), 'Buy Fairy\'s Spirit', ("Kakariko Village", "Kakariko", "Shops"))),
|
||||
|
||||
# Graveyard
|
||||
("Graveyard Shield Grave Chest", ("Chest", 0x40, 0x00, None, 'Hylian Shield', ("the Graveyard", "Kakariko",))),
|
||||
("Graveyard Heart Piece Grave Chest", ("Chest", 0x3F, 0x00, None, 'Piece of Heart', ("the Graveyard", "Kakariko",))),
|
||||
("Graveyard Composers Grave Chest", ("Chest", 0x41, 0x00, None, 'Bombs (5)', ("the Graveyard", "Kakariko",))),
|
||||
("Graveyard Freestanding PoH", ("Collectable", 0x53, 0x04, None, 'Piece of Heart', ("the Graveyard", "Kakariko",))),
|
||||
("Graveyard Dampe Gravedigging Tour", ("Collectable", 0x53, 0x08, None, 'Piece of Heart', ("the Graveyard", "Kakariko",))),
|
||||
("Graveyard Hookshot Chest", ("Chest", 0x48, 0x00, None, 'Progressive Hookshot', ("the Graveyard", "Kakariko",))),
|
||||
("Graveyard Dampe Race Freestanding PoH", ("Collectable", 0x48, 0x07, None, 'Piece of Heart', ("the Graveyard", "Kakariko", "Minigames"))),
|
||||
("Graveyard GS Bean Patch", ("GS Token", 0x10, 0x01, None, 'Gold Skulltula Token', ("the Graveyard", "Skulltulas",))),
|
||||
("Graveyard GS Wall", ("GS Token", 0x10, 0x80, None, 'Gold Skulltula Token', ("the Graveyard", "Skulltulas",))),
|
||||
|
||||
# Death Mountain Trail
|
||||
("DMT Freestanding PoH", ("Collectable", 0x60, 0x1E, None, 'Piece of Heart', ("Death Mountain Trail", "Death Mountain",))),
|
||||
("DMT Chest", ("Chest", 0x60, 0x01, None, 'Rupees (50)', ("Death Mountain Trail", "Death Mountain",))),
|
||||
("DMT Storms Grotto Chest", ("Chest", 0x3E, 0x17, None, 'Rupees (200)', ("Death Mountain Trail", "Death Mountain", "Grottos"))),
|
||||
("DMT Great Fairy Reward", ("Cutscene", 0xFF, 0x13, None, 'Magic Meter', ("Death Mountain Trail", "Death Mountain", "Fairies"))),
|
||||
("DMT Biggoron", ("NPC", 0x60, 0x57, None, 'Biggoron Sword', ("Death Mountain Trail", "Death Mountain",))),
|
||||
("DMT Cow Grotto Cow", ("NPC", 0x3E, 0x15, None, 'Milk', ("Death Mountain Trail", "Death Mountain", "Cow", "Grottos"))),
|
||||
("DMT GS Near Kak", ("GS Token", 0x0F, 0x04, None, 'Gold Skulltula Token', ("Death Mountain Trail", "Skulltulas",))),
|
||||
("DMT GS Bean Patch", ("GS Token", 0x0F, 0x02, None, 'Gold Skulltula Token', ("Death Mountain Trail", "Skulltulas",))),
|
||||
("DMT GS Above Dodongos Cavern", ("GS Token", 0x0F, 0x08, None, 'Gold Skulltula Token', ("Death Mountain Trail", "Skulltulas",))),
|
||||
("DMT GS Falling Rocks Path", ("GS Token", 0x0F, 0x10, None, 'Gold Skulltula Token', ("Death Mountain Trail", "Skulltulas",))),
|
||||
|
||||
# Goron City
|
||||
("GC Darunias Joy", ("NPC", 0x62, 0x54, None, 'Progressive Strength Upgrade', ("Goron City",))),
|
||||
("GC Pot Freestanding PoH", ("Collectable", 0x62, 0x1F, None, 'Piece of Heart', ("Goron City", "Goron City",))),
|
||||
("GC Rolling Goron as Child", ("NPC", 0x62, 0x34, None, 'Bomb Bag', ("Goron City",))),
|
||||
("GC Rolling Goron as Adult", ("NPC", 0x62, 0x2C, None, 'Goron Tunic', ("Goron City",))),
|
||||
("GC Medigoron", ("NPC", 0x62, 0x28, None, 'Giants Knife', ("Goron City",))),
|
||||
("GC Maze Left Chest", ("Chest", 0x62, 0x00, None, 'Rupees (200)', ("Goron City",))),
|
||||
("GC Maze Right Chest", ("Chest", 0x62, 0x01, None, 'Rupees (50)', ("Goron City",))),
|
||||
("GC Maze Center Chest", ("Chest", 0x62, 0x02, None, 'Rupees (50)', ("Goron City",))),
|
||||
("GC Deku Scrub Grotto Left", ("GrottoNPC", 0xFB, 0x30, None, 'Buy Deku Nut (5)', ("Goron City", "Deku Scrub", "Grottos"))),
|
||||
("GC Deku Scrub Grotto Center", ("GrottoNPC", 0xFB, 0x33, None, 'Buy Arrows (30)', ("Goron City", "Deku Scrub", "Grottos"))),
|
||||
("GC Deku Scrub Grotto Right", ("GrottoNPC", 0xFB, 0x37, None, 'Buy Bombs (5) [35]', ("Goron City", "Deku Scrub", "Grottos"))),
|
||||
("GC GS Center Platform", ("GS Token", 0x0F, 0x20, None, 'Gold Skulltula Token', ("Goron City", "Skulltulas",))),
|
||||
("GC GS Boulder Maze", ("GS Token", 0x0F, 0x40, None, 'Gold Skulltula Token', ("Goron City", "Skulltulas",))),
|
||||
("GC Shop Item 1", ("Shop", 0x2E, 0x30, (shop_address(8, 0), None), 'Buy Bombs (5) [25]', ("Goron City", "Shops",))),
|
||||
("GC Shop Item 2", ("Shop", 0x2E, 0x31, (shop_address(8, 1), None), 'Buy Bombs (10)', ("Goron City", "Shops",))),
|
||||
("GC Shop Item 3", ("Shop", 0x2E, 0x32, (shop_address(8, 2), None), 'Buy Bombs (20)', ("Goron City", "Shops",))),
|
||||
("GC Shop Item 4", ("Shop", 0x2E, 0x33, (shop_address(8, 3), None), 'Buy Bombs (30)', ("Goron City", "Shops",))),
|
||||
("GC Shop Item 5", ("Shop", 0x2E, 0x34, (shop_address(8, 4), None), 'Buy Goron Tunic', ("Goron City", "Shops",))),
|
||||
("GC Shop Item 6", ("Shop", 0x2E, 0x35, (shop_address(8, 5), None), 'Buy Heart', ("Goron City", "Shops",))),
|
||||
("GC Shop Item 7", ("Shop", 0x2E, 0x36, (shop_address(8, 6), None), 'Buy Red Potion [40]', ("Goron City", "Shops",))),
|
||||
("GC Shop Item 8", ("Shop", 0x2E, 0x37, (shop_address(8, 7), None), 'Buy Heart', ("Goron City", "Shops",))),
|
||||
|
||||
# Death Mountain Crater
|
||||
("DMC Volcano Freestanding PoH", ("Collectable", 0x61, 0x08, None, 'Piece of Heart', ("Death Mountain Crater", "Death Mountain",))),
|
||||
("DMC Wall Freestanding PoH", ("Collectable", 0x61, 0x02, None, 'Piece of Heart', ("Death Mountain Crater", "Death Mountain",))),
|
||||
("DMC Upper Grotto Chest", ("Chest", 0x3E, 0x1A, None, 'Bombs (20)', ("Death Mountain Crater", "Death Mountain", "Grottos"))),
|
||||
("DMC Great Fairy Reward", ("Cutscene", 0xFF, 0x14, None, 'Magic Meter', ("Death Mountain Crater", "Death Mountain", "Fairies",))),
|
||||
("DMC Deku Scrub", ("NPC", 0x61, 0x37, None, 'Buy Bombs (5) [35]', ("Death Mountain Crater", "Death Mountain", "Deku Scrub"))),
|
||||
("DMC Deku Scrub Grotto Left", ("GrottoNPC", 0xF9, 0x30, None, 'Buy Deku Nut (5)', ("Death Mountain Crater", "Death Mountain", "Deku Scrub", "Grottos"))),
|
||||
("DMC Deku Scrub Grotto Center", ("GrottoNPC", 0xF9, 0x33, None, 'Buy Arrows (30)', ("Death Mountain Crater", "Death Mountain", "Deku Scrub", "Grottos"))),
|
||||
("DMC Deku Scrub Grotto Right", ("GrottoNPC", 0xF9, 0x37, None, 'Buy Bombs (5) [35]', ("Death Mountain Crater", "Death Mountain", "Deku Scrub", "Grottos"))),
|
||||
("DMC GS Crate", ("GS Token", 0x0F, 0x80, None, 'Gold Skulltula Token', ("Death Mountain Crater", "Skulltulas",))),
|
||||
("DMC GS Bean Patch", ("GS Token", 0x0F, 0x01, None, 'Gold Skulltula Token', ("Death Mountain Crater", "Skulltulas",))),
|
||||
|
||||
# Zora's River
|
||||
("ZR Magic Bean Salesman", ("NPC", 0x54, 0x16, None, 'Magic Bean', ("Zora's River",))),
|
||||
("ZR Open Grotto Chest", ("Chest", 0x3E, 0x09, None, 'Rupees (20)', ("Zora's River", "Grottos",))),
|
||||
("ZR Frogs in the Rain", ("NPC", 0x54, 0x3E, None, 'Piece of Heart', ("Zora's River", "Minigames",))),
|
||||
("ZR Frogs Ocarina Game", ("NPC", 0x54, 0x76, None, 'Piece of Heart', ("Zora's River",))),
|
||||
("ZR Near Open Grotto Freestanding PoH", ("Collectable", 0x54, 0x04, None, 'Piece of Heart', ("Zora's River",))),
|
||||
("ZR Near Domain Freestanding PoH", ("Collectable", 0x54, 0x0B, None, 'Piece of Heart', ("Zora's River",))),
|
||||
("ZR Deku Scrub Grotto Front", ("GrottoNPC", 0xEB, 0x3A, None, 'Buy Green Potion', ("Zora's River", "Deku Scrub", "Grottos"))),
|
||||
("ZR Deku Scrub Grotto Rear", ("GrottoNPC", 0xEB, 0x39, None, 'Buy Red Potion [30]', ("Zora's River", "Deku Scrub", "Grottos"))),
|
||||
("ZR GS Tree", ("GS Token", 0x11, 0x02, None, 'Gold Skulltula Token', ("Zora's River", "Skulltulas",))),
|
||||
("ZR GS Ladder", ("GS Token", 0x11, 0x01, None, 'Gold Skulltula Token', ("Zora's River", "Skulltulas",))),
|
||||
("ZR GS Near Raised Grottos", ("GS Token", 0x11, 0x10, None, 'Gold Skulltula Token', ("Zora's River", "Skulltulas",))),
|
||||
("ZR GS Above Bridge", ("GS Token", 0x11, 0x08, None, 'Gold Skulltula Token', ("Zora's River", "Skulltulas",))),
|
||||
|
||||
# Zora's Domain
|
||||
("ZD Diving Minigame", ("NPC", 0x58, 0x37, None, 'Progressive Scale', ("Zora's Domain", "Minigames",))),
|
||||
("ZD Chest", ("Chest", 0x58, 0x00, None, 'Piece of Heart', ("Zora's Domain", ))),
|
||||
("ZD King Zora Thawed", ("NPC", 0x58, 0x2D, None, 'Zora Tunic', ("Zora's Domain",))),
|
||||
("ZD GS Frozen Waterfall", ("GS Token", 0x11, 0x40, None, 'Gold Skulltula Token', ("Zora's Domain", "Skulltulas",))),
|
||||
("ZD Shop Item 1", ("Shop", 0x2F, 0x30, (shop_address(7, 0), None), 'Buy Zora Tunic', ("Zora's Domain", "Shops",))),
|
||||
("ZD Shop Item 2", ("Shop", 0x2F, 0x31, (shop_address(7, 1), None), 'Buy Arrows (10)', ("Zora's Domain", "Shops",))),
|
||||
("ZD Shop Item 3", ("Shop", 0x2F, 0x32, (shop_address(7, 2), None), 'Buy Heart', ("Zora's Domain", "Shops",))),
|
||||
("ZD Shop Item 4", ("Shop", 0x2F, 0x33, (shop_address(7, 3), None), 'Buy Arrows (30)', ("Zora's Domain", "Shops",))),
|
||||
("ZD Shop Item 5", ("Shop", 0x2F, 0x34, (shop_address(7, 4), None), 'Buy Deku Nut (5)', ("Zora's Domain", "Shops",))),
|
||||
("ZD Shop Item 6", ("Shop", 0x2F, 0x35, (shop_address(7, 5), None), 'Buy Arrows (50)', ("Zora's Domain", "Shops",))),
|
||||
("ZD Shop Item 7", ("Shop", 0x2F, 0x36, (shop_address(7, 6), None), 'Buy Fish', ("Zora's Domain", "Shops",))),
|
||||
("ZD Shop Item 8", ("Shop", 0x2F, 0x37, (shop_address(7, 7), None), 'Buy Red Potion [50]', ("Zora's Domain", "Shops",))),
|
||||
|
||||
# Zora's Fountain
|
||||
("ZF Great Fairy Reward", ("Cutscene", 0xFF, 0x10, None, 'Farores Wind', ("Zora's Fountain", "Fairies",))),
|
||||
("ZF Iceberg Freestanding PoH", ("Collectable", 0x59, 0x01, None, 'Piece of Heart', ("Zora's Fountain",))),
|
||||
("ZF Bottom Freestanding PoH", ("Collectable", 0x59, 0x14, None, 'Piece of Heart', ("Zora's Fountain",))),
|
||||
("ZF GS Above the Log", ("GS Token", 0x11, 0x04, None, 'Gold Skulltula Token', ("Zora's Fountain", "Skulltulas",))),
|
||||
("ZF GS Tree", ("GS Token", 0x11, 0x80, None, 'Gold Skulltula Token', ("Zora's Fountain", "Skulltulas",))),
|
||||
("ZF GS Hidden Cave", ("GS Token", 0x11, 0x20, None, 'Gold Skulltula Token', ("Zora's Fountain", "Skulltulas",))),
|
||||
|
||||
# Lake Hylia
|
||||
("LH Underwater Item", ("NPC", 0x57, 0x15, None, 'Rutos Letter', ("Lake Hylia",))),
|
||||
("LH Child Fishing", ("NPC", 0x49, 0x3E, None, 'Piece of Heart', ("Lake Hylia", "Minigames",))),
|
||||
("LH Adult Fishing", ("NPC", 0x49, 0x38, None, 'Progressive Scale', ("Lake Hylia", "Minigames",))),
|
||||
("LH Lab Dive", ("NPC", 0x38, 0x3E, None, 'Piece of Heart', ("Lake Hylia",))),
|
||||
("LH Freestanding PoH", ("Collectable", 0x57, 0x1E, None, 'Piece of Heart', ("Lake Hylia",))),
|
||||
("LH Sun", ("NPC", 0x57, 0x58, None, 'Fire Arrows', ("Lake Hylia",))),
|
||||
("LH Deku Scrub Grotto Left", ("GrottoNPC", 0xEF, 0x30, None, 'Buy Deku Nut (5)', ("Lake Hylia", "Deku Scrub", "Grottos"))),
|
||||
("LH Deku Scrub Grotto Center", ("GrottoNPC", 0xEF, 0x33, None, 'Buy Deku Seeds (30)', ("Lake Hylia", "Deku Scrub", "Grottos"))),
|
||||
("LH Deku Scrub Grotto Right", ("GrottoNPC", 0xEF, 0x37, None, 'Buy Bombs (5) [35]', ("Lake Hylia", "Deku Scrub", "Grottos"))),
|
||||
("LH GS Bean Patch", ("GS Token", 0x12, 0x01, None, 'Gold Skulltula Token', ("Lake Hylia", "Skulltulas",))),
|
||||
("LH GS Lab Wall", ("GS Token", 0x12, 0x04, None, 'Gold Skulltula Token', ("Lake Hylia", "Skulltulas",))),
|
||||
("LH GS Small Island", ("GS Token", 0x12, 0x02, None, 'Gold Skulltula Token', ("Lake Hylia", "Skulltulas",))),
|
||||
("LH GS Lab Crate", ("GS Token", 0x12, 0x08, None, 'Gold Skulltula Token', ("Lake Hylia", "Skulltulas",))),
|
||||
("LH GS Tree", ("GS Token", 0x12, 0x10, None, 'Gold Skulltula Token', ("Lake Hylia", "Skulltulas",))),
|
||||
|
||||
# Gerudo Valley
|
||||
("GV Crate Freestanding PoH", ("Collectable", 0x5A, 0x02, None, 'Piece of Heart', ("Gerudo Valley", "Gerudo",))),
|
||||
("GV Waterfall Freestanding PoH", ("Collectable", 0x5A, 0x01, None, 'Piece of Heart', ("Gerudo Valley", "Gerudo",))),
|
||||
("GV Chest", ("Chest", 0x5A, 0x00, None, 'Rupees (50)', ("Gerudo Valley", "Gerudo",))),
|
||||
("GV Deku Scrub Grotto Front", ("GrottoNPC", 0xF0, 0x3A, None, 'Buy Green Potion', ("Gerudo Valley", "Gerudo", "Deku Scrub", "Grottos"))),
|
||||
("GV Deku Scrub Grotto Rear", ("GrottoNPC", 0xF0, 0x39, None, 'Buy Red Potion [30]', ("Gerudo Valley", "Gerudo", "Deku Scrub", "Grottos"))),
|
||||
("GV Cow", ("NPC", 0x5A, 0x15, None, 'Milk', ("Gerudo Valley", "Gerudo", "Cow"))),
|
||||
("GV GS Small Bridge", ("GS Token", 0x13, 0x02, None, 'Gold Skulltula Token', ("Gerudo Valley", "Skulltulas",))),
|
||||
("GV GS Bean Patch", ("GS Token", 0x13, 0x01, None, 'Gold Skulltula Token', ("Gerudo Valley", "Skulltulas",))),
|
||||
("GV GS Behind Tent", ("GS Token", 0x13, 0x08, None, 'Gold Skulltula Token', ("Gerudo Valley", "Skulltulas",))),
|
||||
("GV GS Pillar", ("GS Token", 0x13, 0x04, None, 'Gold Skulltula Token', ("Gerudo Valley", "Skulltulas",))),
|
||||
|
||||
# Gerudo's Fortress
|
||||
("GF North F1 Carpenter", ("Collectable", 0x0C, 0x0C, None, 'Small Key (Gerudo Fortress)', ("Gerudo's Fortress", "Gerudo",))),
|
||||
("GF North F2 Carpenter", ("Collectable", 0x0C, 0x0A, None, 'Small Key (Gerudo Fortress)', ("Gerudo's Fortress", "Gerudo",))),
|
||||
("GF South F1 Carpenter", ("Collectable", 0x0C, 0x0E, None, 'Small Key (Gerudo Fortress)', ("Gerudo's Fortress", "Gerudo",))),
|
||||
("GF South F2 Carpenter", ("Collectable", 0x0C, 0x0F, None, 'Small Key (Gerudo Fortress)', ("Gerudo's Fortress", "Gerudo",))),
|
||||
("GF Gerudo Membership Card", ("NPC", 0x0C, 0x3A, None, 'Gerudo Membership Card', ("Gerudo's Fortress", "Gerudo",))),
|
||||
("GF Chest", ("Chest", 0x5D, 0x00, None, 'Piece of Heart', ("Gerudo's Fortress", "Gerudo",))),
|
||||
("GF HBA 1000 Points", ("NPC", 0x5D, 0x3E, None, 'Piece of Heart', ("Gerudo's Fortress", "Gerudo", "Minigames"))),
|
||||
("GF HBA 1500 Points", ("NPC", 0x5D, 0x30, None, 'Bow', ("Gerudo's Fortress", "Gerudo", "Minigames"))),
|
||||
("GF GS Top Floor", ("GS Token", 0x14, 0x02, None, 'Gold Skulltula Token', ("Gerudo's Fortress", "Skulltulas",))),
|
||||
("GF GS Archery Range", ("GS Token", 0x14, 0x01, None, 'Gold Skulltula Token', ("Gerudo's Fortress", "Skulltulas",))),
|
||||
|
||||
# Wasteland
|
||||
("Wasteland Bombchu Salesman", ("NPC", 0x5E, 0x03, None, 'Bombchus (10)', ("Haunted Wasteland",))),
|
||||
("Wasteland Chest", ("Chest", 0x5E, 0x00, None, 'Rupees (50)', ("Haunted Wasteland",))),
|
||||
("Wasteland GS", ("GS Token", 0x15, 0x02, None, 'Gold Skulltula Token', ("Haunted Wasteland", "Skulltulas",))),
|
||||
|
||||
# Colossus
|
||||
("Colossus Great Fairy Reward", ("Cutscene", 0xFF, 0x12, None, 'Nayrus Love', ("Desert Colossus", "Fairies",))),
|
||||
("Colossus Freestanding PoH", ("Collectable", 0x5C, 0x0D, None, 'Piece of Heart', ("Desert Colossus",))),
|
||||
("Colossus Deku Scrub Grotto Front", ("GrottoNPC", 0xFD, 0x3A, None, 'Buy Green Potion', ("Desert Colossus", "Deku Scrub", "Grottos"))),
|
||||
("Colossus Deku Scrub Grotto Rear", ("GrottoNPC", 0xFD, 0x39, None, 'Buy Red Potion [30]', ("Desert Colossus", "Deku Scrub", "Grottos"))),
|
||||
("Colossus GS Bean Patch", ("GS Token", 0x15, 0x01, None, 'Gold Skulltula Token', ("Desert Colossus", "Skulltulas",))),
|
||||
("Colossus GS Tree", ("GS Token", 0x15, 0x08, None, 'Gold Skulltula Token', ("Desert Colossus", "Skulltulas",))),
|
||||
("Colossus GS Hill", ("GS Token", 0x15, 0x04, None, 'Gold Skulltula Token', ("Desert Colossus", "Skulltulas",))),
|
||||
|
||||
# Outside Ganon's Castle
|
||||
("OGC Great Fairy Reward", ("Cutscene", 0xFF, 0x15, None, 'Double Defense', ("outside Ganon's Castle", "Market", "Fairies"))),
|
||||
("OGC GS", ("GS Token", 0x0E, 0x01, None, 'Gold Skulltula Token', ("outside Ganon's Castle", "Skulltulas",))),
|
||||
|
||||
## Dungeons
|
||||
# Deku Tree vanilla
|
||||
("Deku Tree Map Chest", ("Chest", 0x00, 0x03, None, 'Map (Deku Tree)', ("Deku Tree", "Vanilla",))),
|
||||
("Deku Tree Slingshot Room Side Chest", ("Chest", 0x00, 0x05, None, 'Recovery Heart', ("Deku Tree", "Vanilla",))),
|
||||
("Deku Tree Slingshot Chest", ("Chest", 0x00, 0x01, None, 'Slingshot', ("Deku Tree", "Vanilla",))),
|
||||
("Deku Tree Compass Chest", ("Chest", 0x00, 0x02, None, 'Compass (Deku Tree)', ("Deku Tree", "Vanilla",))),
|
||||
("Deku Tree Compass Room Side Chest", ("Chest", 0x00, 0x06, None, 'Recovery Heart', ("Deku Tree", "Vanilla",))),
|
||||
("Deku Tree Basement Chest", ("Chest", 0x00, 0x04, None, 'Recovery Heart', ("Deku Tree", "Vanilla",))),
|
||||
("Deku Tree GS Compass Room", ("GS Token", 0x00, 0x08, None, 'Gold Skulltula Token', ("Deku Tree", "Vanilla", "Skulltulas",))),
|
||||
("Deku Tree GS Basement Vines", ("GS Token", 0x00, 0x04, None, 'Gold Skulltula Token', ("Deku Tree", "Vanilla", "Skulltulas",))),
|
||||
("Deku Tree GS Basement Gate", ("GS Token", 0x00, 0x02, None, 'Gold Skulltula Token', ("Deku Tree", "Vanilla", "Skulltulas",))),
|
||||
("Deku Tree GS Basement Back Room", ("GS Token", 0x00, 0x01, None, 'Gold Skulltula Token', ("Deku Tree", "Vanilla", "Skulltulas",))),
|
||||
# Deku Tree MQ
|
||||
("Deku Tree MQ Map Chest", ("Chest", 0x00, 0x03, None, 'Map (Deku Tree)', ("Deku Tree", "Master Quest",))),
|
||||
("Deku Tree MQ Slingshot Chest", ("Chest", 0x00, 0x06, None, 'Slingshot', ("Deku Tree", "Master Quest",))),
|
||||
("Deku Tree MQ Slingshot Room Back Chest", ("Chest", 0x00, 0x02, None, 'Deku Shield', ("Deku Tree", "Master Quest",))),
|
||||
("Deku Tree MQ Compass Chest", ("Chest", 0x00, 0x01, None, 'Compass (Deku Tree)', ("Deku Tree", "Master Quest",))),
|
||||
("Deku Tree MQ Basement Chest", ("Chest", 0x00, 0x04, None, 'Deku Shield', ("Deku Tree", "Master Quest",))),
|
||||
("Deku Tree MQ Before Spinning Log Chest", ("Chest", 0x00, 0x05, None, 'Recovery Heart', ("Deku Tree", "Master Quest",))),
|
||||
("Deku Tree MQ After Spinning Log Chest", ("Chest", 0x00, 0x00, None, 'Rupees (50)', ("Deku Tree", "Master Quest",))),
|
||||
("Deku Tree MQ Deku Scrub", ("NPC", 0x00, 0x34, None, 'Buy Deku Shield', ("Deku Tree", "Master Quest", "Deku Scrub",))),
|
||||
("Deku Tree MQ GS Lobby", ("GS Token", 0x00, 0x02, None, 'Gold Skulltula Token', ("Deku Tree", "Master Quest", "Skulltulas",))),
|
||||
("Deku Tree MQ GS Compass Room", ("GS Token", 0x00, 0x08, None, 'Gold Skulltula Token', ("Deku Tree", "Master Quest", "Skulltulas",))),
|
||||
("Deku Tree MQ GS Basement Graves Room", ("GS Token", 0x00, 0x04, None, 'Gold Skulltula Token', ("Deku Tree", "Master Quest", "Skulltulas",))),
|
||||
("Deku Tree MQ GS Basement Back Room", ("GS Token", 0x00, 0x01, None, 'Gold Skulltula Token', ("Deku Tree", "Master Quest", "Skulltulas",))),
|
||||
# Deku Tree shared
|
||||
("Deku Tree Queen Gohma Heart", ("BossHeart", 0x11, 0x4F, None, 'Heart Container', ("Deku Tree", "Vanilla", "Master Quest",))),
|
||||
|
||||
# Dodongo's Cavern vanilla
|
||||
("Dodongos Cavern Map Chest", ("Chest", 0x01, 0x08, None, 'Map (Dodongos Cavern)', ("Dodongo's Cavern", "Vanilla",))),
|
||||
("Dodongos Cavern Compass Chest", ("Chest", 0x01, 0x05, None, 'Compass (Dodongos Cavern)', ("Dodongo's Cavern", "Vanilla",))),
|
||||
("Dodongos Cavern Bomb Flower Platform Chest", ("Chest", 0x01, 0x06, None, 'Rupees (20)', ("Dodongo's Cavern", "Vanilla",))),
|
||||
("Dodongos Cavern Bomb Bag Chest", ("Chest", 0x01, 0x04, None, 'Bomb Bag', ("Dodongo's Cavern", "Vanilla",))),
|
||||
("Dodongos Cavern End of Bridge Chest", ("Chest", 0x01, 0x0A, None, 'Deku Shield', ("Dodongo's Cavern", "Vanilla",))),
|
||||
("Dodongos Cavern Deku Scrub Side Room Near Dodongos", ("NPC", 0x01, 0x31, None, 'Buy Deku Stick (1)', ("Dodongo's Cavern", "Vanilla", "Deku Scrub",))),
|
||||
("Dodongos Cavern Deku Scrub Lobby", ("NPC", 0x01, 0x34, None, 'Buy Deku Shield', ("Dodongo's Cavern", "Vanilla", "Deku Scrub",))),
|
||||
("Dodongos Cavern Deku Scrub Near Bomb Bag Left", ("NPC", 0x01, 0x30, None, 'Buy Deku Nut (5)', ("Dodongo's Cavern", "Vanilla", "Deku Scrub",))),
|
||||
("Dodongos Cavern Deku Scrub Near Bomb Bag Right", ("NPC", 0x01, 0x33, None, 'Buy Deku Seeds (30)', ("Dodongo's Cavern", "Vanilla", "Deku Scrub",))),
|
||||
("Dodongos Cavern GS Side Room Near Lower Lizalfos", ("GS Token", 0x01, 0x10, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Vanilla", "Skulltulas",))),
|
||||
("Dodongos Cavern GS Scarecrow", ("GS Token", 0x01, 0x02, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Vanilla", "Skulltulas",))),
|
||||
("Dodongos Cavern GS Alcove Above Stairs", ("GS Token", 0x01, 0x04, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Vanilla", "Skulltulas",))),
|
||||
("Dodongos Cavern GS Vines Above Stairs", ("GS Token", 0x01, 0x01, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Vanilla", "Skulltulas",))),
|
||||
("Dodongos Cavern GS Back Room", ("GS Token", 0x01, 0x08, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Vanilla", "Skulltulas",))),
|
||||
# Dodongo's Cavern MQ
|
||||
("Dodongos Cavern MQ Map Chest", ("Chest", 0x01, 0x00, None, 'Map (Dodongos Cavern)', ("Dodongo's Cavern", "Master Quest",))),
|
||||
("Dodongos Cavern MQ Bomb Bag Chest", ("Chest", 0x01, 0x04, None, 'Bomb Bag', ("Dodongo's Cavern", "Master Quest",))),
|
||||
("Dodongos Cavern MQ Torch Puzzle Room Chest", ("Chest", 0x01, 0x03, None, 'Rupees (5)', ("Dodongo's Cavern", "Master Quest",))),
|
||||
("Dodongos Cavern MQ Larvae Room Chest", ("Chest", 0x01, 0x02, None, 'Deku Shield', ("Dodongo's Cavern", "Master Quest",))),
|
||||
("Dodongos Cavern MQ Compass Chest", ("Chest", 0x01, 0x05, None, 'Compass (Dodongos Cavern)', ("Dodongo's Cavern", "Master Quest",))),
|
||||
("Dodongos Cavern MQ Under Grave Chest", ("Chest", 0x01, 0x01, None, 'Hylian Shield', ("Dodongo's Cavern", "Master Quest",))),
|
||||
("Dodongos Cavern MQ Deku Scrub Lobby Front", ("NPC", 0x01, 0x33, None, 'Buy Deku Seeds (30)', ("Dodongo's Cavern", "Master Quest", "Deku Scrub",))),
|
||||
("Dodongos Cavern MQ Deku Scrub Lobby Rear", ("NPC", 0x01, 0x31, None, 'Buy Deku Stick (1)', ("Dodongo's Cavern", "Master Quest", "Deku Scrub",))),
|
||||
("Dodongos Cavern MQ Deku Scrub Side Room Near Lower Lizalfos", ("NPC", 0x01, 0x39, None, 'Buy Red Potion [30]', ("Dodongo's Cavern", "Master Quest", "Deku Scrub",))),
|
||||
("Dodongos Cavern MQ Deku Scrub Staircase", ("NPC", 0x01, 0x34, None, 'Buy Deku Shield', ("Dodongo's Cavern", "Master Quest", "Deku Scrub",))),
|
||||
("Dodongos Cavern MQ GS Scrub Room", ("GS Token", 0x01, 0x02, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Master Quest", "Skulltulas",))),
|
||||
("Dodongos Cavern MQ GS Larvae Room", ("GS Token", 0x01, 0x10, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Master Quest", "Skulltulas",))),
|
||||
("Dodongos Cavern MQ GS Lizalfos Room", ("GS Token", 0x01, 0x04, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Master Quest", "Skulltulas",))),
|
||||
("Dodongos Cavern MQ GS Song of Time Block Room", ("GS Token", 0x01, 0x08, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Master Quest", "Skulltulas",))),
|
||||
("Dodongos Cavern MQ GS Back Area", ("GS Token", 0x01, 0x01, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Master Quest", "Skulltulas",))),
|
||||
# Dodongo's Cavern shared
|
||||
("Dodongos Cavern Boss Room Chest", ("Chest", 0x12, 0x00, None, 'Bombs (5)', ("Dodongo's Cavern", "Vanilla", "Master Quest",))),
|
||||
("Dodongos Cavern King Dodongo Heart", ("BossHeart", 0x12, 0x4F, None, 'Heart Container', ("Dodongo's Cavern", "Vanilla", "Master Quest",))),
|
||||
|
||||
# Jabu Jabu's Belly vanilla
|
||||
("Jabu Jabus Belly Boomerang Chest", ("Chest", 0x02, 0x01, None, 'Boomerang', ("Jabu Jabu's Belly", "Vanilla",))),
|
||||
("Jabu Jabus Belly Map Chest", ("Chest", 0x02, 0x02, None, 'Map (Jabu Jabus Belly)', ("Jabu Jabu's Belly", "Vanilla",))),
|
||||
("Jabu Jabus Belly Compass Chest", ("Chest", 0x02, 0x04, None, 'Compass (Jabu Jabus Belly)', ("Jabu Jabu's Belly", "Vanilla",))),
|
||||
("Jabu Jabus Belly Deku Scrub", ("NPC", 0x02, 0x30, None, 'Buy Deku Nut (5)', ("Jabu Jabu's Belly", "Vanilla", "Deku Scrub",))),
|
||||
("Jabu Jabus Belly GS Water Switch Room", ("GS Token", 0x02, 0x08, None, 'Gold Skulltula Token', ("Jabu Jabu's Belly", "Vanilla", "Skulltulas",))),
|
||||
("Jabu Jabus Belly GS Lobby Basement Lower", ("GS Token", 0x02, 0x01, None, 'Gold Skulltula Token', ("Jabu Jabu's Belly", "Vanilla", "Skulltulas",))),
|
||||
("Jabu Jabus Belly GS Lobby Basement Upper", ("GS Token", 0x02, 0x02, None, 'Gold Skulltula Token', ("Jabu Jabu's Belly", "Vanilla", "Skulltulas",))),
|
||||
("Jabu Jabus Belly GS Near Boss", ("GS Token", 0x02, 0x04, None, 'Gold Skulltula Token', ("Jabu Jabu's Belly", "Vanilla", "Skulltulas",))),
|
||||
# Jabu Jabu's Belly MQ
|
||||
("Jabu Jabus Belly MQ Map Chest", ("Chest", 0x02, 0x03, None, 'Map (Jabu Jabus Belly)', ("Jabu Jabu's Belly", "Master Quest",))),
|
||||
("Jabu Jabus Belly MQ First Room Side Chest", ("Chest", 0x02, 0x05, None, 'Deku Nuts (5)', ("Jabu Jabu's Belly", "Master Quest",))),
|
||||
("Jabu Jabus Belly MQ Second Room Lower Chest", ("Chest", 0x02, 0x02, None, 'Deku Nuts (5)', ("Jabu Jabu's Belly", "Master Quest",))),
|
||||
("Jabu Jabus Belly MQ Compass Chest", ("Chest", 0x02, 0x00, None, 'Compass (Jabu Jabus Belly)', ("Jabu Jabu's Belly", "Master Quest",))),
|
||||
("Jabu Jabus Belly MQ Basement Near Switches Chest", ("Chest", 0x02, 0x08, None, 'Deku Nuts (5)', ("Jabu Jabu's Belly", "Master Quest",))),
|
||||
("Jabu Jabus Belly MQ Basement Near Vines Chest", ("Chest", 0x02, 0x04, None, 'Bombchus (10)', ("Jabu Jabu's Belly", "Master Quest",))),
|
||||
("Jabu Jabus Belly MQ Boomerang Room Small Chest", ("Chest", 0x02, 0x01, None, 'Deku Nuts (5)', ("Jabu Jabu's Belly", "Master Quest",))),
|
||||
("Jabu Jabus Belly MQ Boomerang Chest", ("Chest", 0x02, 0x06, None, 'Boomerang', ("Jabu Jabu's Belly", "Master Quest",))),
|
||||
("Jabu Jabus Belly MQ Falling Like Like Room Chest", ("Chest", 0x02, 0x09, None, 'Deku Stick (1)', ("Jabu Jabu's Belly", "Master Quest",))),
|
||||
("Jabu Jabus Belly MQ Second Room Upper Chest", ("Chest", 0x02, 0x07, None, 'Recovery Heart', ("Jabu Jabu's Belly", "Master Quest",))),
|
||||
("Jabu Jabus Belly MQ Near Boss Chest", ("Chest", 0x02, 0x0A, None, 'Deku Shield', ("Jabu Jabu's Belly", "Master Quest",))),
|
||||
("Jabu Jabus Belly MQ Cow", ("NPC", 0x02, 0x15, None, 'Milk', ("Jabu Jabu's Belly", "Master Quest", "Cow",))),
|
||||
("Jabu Jabus Belly MQ GS Boomerang Chest Room", ("GS Token", 0x02, 0x01, None, 'Gold Skulltula Token', ("Jabu Jabu's Belly", "Master Quest", "Skulltulas",))),
|
||||
("Jabu Jabus Belly MQ GS Tailpasaran Room", ("GS Token", 0x02, 0x04, None, 'Gold Skulltula Token', ("Jabu Jabu's Belly", "Master Quest", "Skulltulas",))),
|
||||
("Jabu Jabus Belly MQ GS Invisible Enemies Room", ("GS Token", 0x02, 0x08, None, 'Gold Skulltula Token', ("Jabu Jabu's Belly", "Master Quest", "Skulltulas",))),
|
||||
("Jabu Jabus Belly MQ GS Near Boss", ("GS Token", 0x02, 0x02, None, 'Gold Skulltula Token', ("Jabu Jabu's Belly", "Master Quest", "Skulltulas",))),
|
||||
# Jabu Jabu's Belly shared
|
||||
("Jabu Jabus Belly Barinade Heart", ("BossHeart", 0x13, 0x4F, None, 'Heart Container', ("Jabu Jabu's Belly", "Vanilla", "Master Quest",))),
|
||||
|
||||
# Bottom of the Well vanilla
|
||||
("Bottom of the Well Front Left Fake Wall Chest", ("Chest", 0x08, 0x08, None, 'Small Key (Bottom of the Well)', ("Bottom of the Well", "Vanilla",))),
|
||||
("Bottom of the Well Front Center Bombable Chest", ("Chest", 0x08, 0x02, None, 'Bombchus (10)', ("Bottom of the Well", "Vanilla",))),
|
||||
("Bottom of the Well Back Left Bombable Chest", ("Chest", 0x08, 0x04, None, 'Deku Nuts (10)', ("Bottom of the Well", "Vanilla",))),
|
||||
("Bottom of the Well Underwater Left Chest", ("Chest", 0x08, 0x09, None, 'Recovery Heart', ("Bottom of the Well", "Vanilla",))),
|
||||
("Bottom of the Well Freestanding Key", ("Collectable", 0x08, 0x01, None, 'Small Key (Bottom of the Well)', ("Bottom of the Well", "Vanilla",))),
|
||||
("Bottom of the Well Compass Chest", ("Chest", 0x08, 0x01, None, 'Compass (Bottom of the Well)', ("Bottom of the Well", "Vanilla",))),
|
||||
("Bottom of the Well Center Skulltula Chest", ("Chest", 0x08, 0x0E, None, 'Deku Nuts (5)', ("Bottom of the Well", "Vanilla",))),
|
||||
("Bottom of the Well Right Bottom Fake Wall Chest", ("Chest", 0x08, 0x05, None, 'Small Key (Bottom of the Well)', ("Bottom of the Well", "Vanilla",))),
|
||||
("Bottom of the Well Fire Keese Chest", ("Chest", 0x08, 0x0A, None, 'Deku Shield', ("Bottom of the Well", "Vanilla",))),
|
||||
("Bottom of the Well Like Like Chest", ("Chest", 0x08, 0x0C, None, 'Hylian Shield', ("Bottom of the Well", "Vanilla",))),
|
||||
("Bottom of the Well Map Chest", ("Chest", 0x08, 0x07, None, 'Map (Bottom of the Well)', ("Bottom of the Well", "Vanilla",))),
|
||||
("Bottom of the Well Underwater Front Chest", ("Chest", 0x08, 0x10, None, 'Bombs (10)', ("Bottom of the Well", "Vanilla",))),
|
||||
("Bottom of the Well Invisible Chest", ("Chest", 0x08, 0x14, None, 'Rupees (200)', ("Bottom of the Well", "Vanilla",))),
|
||||
("Bottom of the Well Lens of Truth Chest", ("Chest", 0x08, 0x03, None, 'Lens of Truth', ("Bottom of the Well", "Vanilla",))),
|
||||
("Bottom of the Well GS West Inner Room", ("GS Token", 0x08, 0x04, None, 'Gold Skulltula Token', ("Bottom of the Well", "Vanilla", "Skulltulas",))),
|
||||
("Bottom of the Well GS East Inner Room", ("GS Token", 0x08, 0x02, None, 'Gold Skulltula Token', ("Bottom of the Well", "Vanilla", "Skulltulas",))),
|
||||
("Bottom of the Well GS Like Like Cage", ("GS Token", 0x08, 0x01, None, 'Gold Skulltula Token', ("Bottom of the Well", "Vanilla", "Skulltulas",))),
|
||||
# Bottom of the Well MQ
|
||||
("Bottom of the Well MQ Map Chest", ("Chest", 0x08, 0x03, None, 'Map (Bottom of the Well)', ("Bottom of the Well", "Master Quest",))),
|
||||
("Bottom of the Well MQ East Inner Room Freestanding Key", ("Collectable", 0x08, 0x01, None, 'Small Key (Bottom of the Well)', ("Bottom of the Well", "Master Quest",))),
|
||||
("Bottom of the Well MQ Compass Chest", ("Chest", 0x08, 0x02, None, 'Compass (Bottom of the Well)', ("Bottom of the Well", "Master Quest",))),
|
||||
("Bottom of the Well MQ Dead Hand Freestanding Key", ("Collectable", 0x08, 0x02, None, 'Small Key (Bottom of the Well)', ("Bottom of the Well", "Master Quest",))),
|
||||
("Bottom of the Well MQ Lens of Truth Chest", ("Chest", 0x08, 0x01, None, 'Lens of Truth', ("Bottom of the Well", "Master Quest",))),
|
||||
("Bottom of the Well MQ GS Coffin Room", ("GS Token", 0x08, 0x04, None, 'Gold Skulltula Token', ("Bottom of the Well", "Master Quest", "Skulltulas",))),
|
||||
("Bottom of the Well MQ GS West Inner Room", ("GS Token", 0x08, 0x02, None, 'Gold Skulltula Token', ("Bottom of the Well", "Master Quest", "Skulltulas",))),
|
||||
("Bottom of the Well MQ GS Basement", ("GS Token", 0x08, 0x01, None, 'Gold Skulltula Token', ("Bottom of the Well", "Master Quest", "Skulltulas",))),
|
||||
|
||||
# Forest Temple vanilla
|
||||
("Forest Temple First Room Chest", ("Chest", 0x03, 0x03, None, 'Small Key (Forest Temple)', ("Forest Temple", "Vanilla",))),
|
||||
("Forest Temple First Stalfos Chest", ("Chest", 0x03, 0x00, None, 'Small Key (Forest Temple)', ("Forest Temple", "Vanilla",))),
|
||||
("Forest Temple Raised Island Courtyard Chest", ("Chest", 0x03, 0x05, None, 'Recovery Heart', ("Forest Temple", "Vanilla",))),
|
||||
("Forest Temple Map Chest", ("Chest", 0x03, 0x01, None, 'Map (Forest Temple)', ("Forest Temple", "Vanilla",))),
|
||||
("Forest Temple Well Chest", ("Chest", 0x03, 0x09, None, 'Small Key (Forest Temple)', ("Forest Temple", "Vanilla",))),
|
||||
("Forest Temple Eye Switch Chest", ("Chest", 0x03, 0x04, None, 'Arrows (30)', ("Forest Temple", "Vanilla",))),
|
||||
("Forest Temple Boss Key Chest", ("Chest", 0x03, 0x0E, None, 'Boss Key (Forest Temple)', ("Forest Temple", "Vanilla",))),
|
||||
("Forest Temple Floormaster Chest", ("Chest", 0x03, 0x02, None, 'Small Key (Forest Temple)', ("Forest Temple", "Vanilla",))),
|
||||
("Forest Temple Red Poe Chest", ("Chest", 0x03, 0x0D, None, 'Small Key (Forest Temple)', ("Forest Temple", "Vanilla",))),
|
||||
("Forest Temple Bow Chest", ("Chest", 0x03, 0x0C, None, 'Bow', ("Forest Temple", "Vanilla",))),
|
||||
("Forest Temple Blue Poe Chest", ("Chest", 0x03, 0x0F, None, 'Compass (Forest Temple)', ("Forest Temple", "Vanilla",))),
|
||||
("Forest Temple Falling Ceiling Room Chest", ("Chest", 0x03, 0x07, None, 'Arrows (10)', ("Forest Temple", "Vanilla",))),
|
||||
("Forest Temple Basement Chest", ("Chest", 0x03, 0x0B, None, 'Arrows (5)', ("Forest Temple", "Vanilla",))),
|
||||
("Forest Temple GS First Room", ("GS Token", 0x03, 0x02, None, 'Gold Skulltula Token', ("Forest Temple", "Vanilla", "Skulltulas",))),
|
||||
("Forest Temple GS Lobby", ("GS Token", 0x03, 0x08, None, 'Gold Skulltula Token', ("Forest Temple", "Vanilla", "Skulltulas",))),
|
||||
("Forest Temple GS Raised Island Courtyard", ("GS Token", 0x03, 0x01, None, 'Gold Skulltula Token', ("Forest Temple", "Vanilla", "Skulltulas",))),
|
||||
("Forest Temple GS Level Island Courtyard", ("GS Token", 0x03, 0x04, None, 'Gold Skulltula Token', ("Forest Temple", "Vanilla", "Skulltulas",))),
|
||||
("Forest Temple GS Basement", ("GS Token", 0x03, 0x10, None, 'Gold Skulltula Token', ("Forest Temple", "Vanilla", "Skulltulas",))),
|
||||
# Forest Temple MQ
|
||||
("Forest Temple MQ First Room Chest", ("Chest", 0x03, 0x03, None, 'Small Key (Forest Temple)', ("Forest Temple", "Master Quest",))),
|
||||
("Forest Temple MQ Wolfos Chest", ("Chest", 0x03, 0x00, None, 'Small Key (Forest Temple)', ("Forest Temple", "Master Quest",))),
|
||||
("Forest Temple MQ Well Chest", ("Chest", 0x03, 0x09, None, 'Small Key (Forest Temple)', ("Forest Temple", "Master Quest",))),
|
||||
("Forest Temple MQ Raised Island Courtyard Lower Chest", ("Chest", 0x03, 0x01, None, 'Small Key (Forest Temple)', ("Forest Temple", "Master Quest",))),
|
||||
("Forest Temple MQ Raised Island Courtyard Upper Chest", ("Chest", 0x03, 0x05, None, 'Small Key (Forest Temple)', ("Forest Temple", "Master Quest",))),
|
||||
("Forest Temple MQ Boss Key Chest", ("Chest", 0x03, 0x0E, None, 'Boss Key (Forest Temple)', ("Forest Temple", "Master Quest",))),
|
||||
("Forest Temple MQ Redead Chest", ("Chest", 0x03, 0x02, None, 'Small Key (Forest Temple)', ("Forest Temple", "Master Quest",))),
|
||||
("Forest Temple MQ Map Chest", ("Chest", 0x03, 0x0D, None, 'Map (Forest Temple)', ("Forest Temple", "Master Quest",))),
|
||||
("Forest Temple MQ Bow Chest", ("Chest", 0x03, 0x0C, None, 'Bow', ("Forest Temple", "Master Quest",))),
|
||||
("Forest Temple MQ Compass Chest", ("Chest", 0x03, 0x0F, None, 'Compass (Forest Temple)', ("Forest Temple", "Master Quest",))),
|
||||
("Forest Temple MQ Falling Ceiling Room Chest", ("Chest", 0x03, 0x06, None, 'Arrows (5)', ("Forest Temple", "Master Quest",))),
|
||||
("Forest Temple MQ Basement Chest", ("Chest", 0x03, 0x0B, None, 'Arrows (5)', ("Forest Temple", "Master Quest",))),
|
||||
("Forest Temple MQ GS First Hallway", ("GS Token", 0x03, 0x02, None, 'Gold Skulltula Token', ("Forest Temple", "Master Quest", "Skulltulas",))),
|
||||
("Forest Temple MQ GS Raised Island Courtyard", ("GS Token", 0x03, 0x01, None, 'Gold Skulltula Token', ("Forest Temple", "Master Quest", "Skulltulas",))),
|
||||
("Forest Temple MQ GS Level Island Courtyard", ("GS Token", 0x03, 0x04, None, 'Gold Skulltula Token', ("Forest Temple", "Master Quest", "Skulltulas",))),
|
||||
("Forest Temple MQ GS Well", ("GS Token", 0x03, 0x08, None, 'Gold Skulltula Token', ("Forest Temple", "Master Quest", "Skulltulas",))),
|
||||
("Forest Temple MQ GS Block Push Room", ("GS Token", 0x03, 0x10, None, 'Gold Skulltula Token', ("Forest Temple", "Master Quest", "Skulltulas",))),
|
||||
# Forest Temple shared
|
||||
("Forest Temple Phantom Ganon Heart", ("BossHeart", 0x14, 0x4F, None, 'Heart Container', ("Forest Temple", "Vanilla", "Master Quest",))),
|
||||
|
||||
# Fire Temple vanilla
|
||||
("Fire Temple Near Boss Chest", ("Chest", 0x04, 0x01, None, 'Small Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
|
||||
("Fire Temple Flare Dancer Chest", ("Chest", 0x04, 0x00, None, 'Bombs (10)', ("Fire Temple", "Vanilla",))),
|
||||
("Fire Temple Boss Key Chest", ("Chest", 0x04, 0x0C, None, 'Boss Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
|
||||
("Fire Temple Big Lava Room Lower Open Door Chest", ("Chest", 0x04, 0x04, None, 'Small Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
|
||||
("Fire Temple Big Lava Room Blocked Door Chest", ("Chest", 0x04, 0x02, None, 'Small Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
|
||||
("Fire Temple Boulder Maze Lower Chest", ("Chest", 0x04, 0x03, None, 'Small Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
|
||||
("Fire Temple Boulder Maze Side Room Chest", ("Chest", 0x04, 0x08, None, 'Small Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
|
||||
("Fire Temple Map Chest", ("Chest", 0x04, 0x0A, None, 'Map (Fire Temple)', ("Fire Temple", "Vanilla",))),
|
||||
("Fire Temple Boulder Maze Shortcut Chest", ("Chest", 0x04, 0x0B, None, 'Small Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
|
||||
("Fire Temple Boulder Maze Upper Chest", ("Chest", 0x04, 0x06, None, 'Small Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
|
||||
("Fire Temple Scarecrow Chest", ("Chest", 0x04, 0x0D, None, 'Rupees (200)', ("Fire Temple", "Vanilla",))),
|
||||
("Fire Temple Compass Chest", ("Chest", 0x04, 0x07, None, 'Compass (Fire Temple)', ("Fire Temple", "Vanilla",))),
|
||||
("Fire Temple Megaton Hammer Chest", ("Chest", 0x04, 0x05, None, 'Megaton Hammer', ("Fire Temple", "Vanilla",))),
|
||||
("Fire Temple Highest Goron Chest", ("Chest", 0x04, 0x09, None, 'Small Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
|
||||
("Fire Temple GS Boss Key Loop", ("GS Token", 0x04, 0x02, None, 'Gold Skulltula Token', ("Fire Temple", "Vanilla", "Skulltulas",))),
|
||||
("Fire Temple GS Song of Time Room", ("GS Token", 0x04, 0x01, None, 'Gold Skulltula Token', ("Fire Temple", "Vanilla", "Skulltulas",))),
|
||||
("Fire Temple GS Boulder Maze", ("GS Token", 0x04, 0x04, None, 'Gold Skulltula Token', ("Fire Temple", "Vanilla", "Skulltulas",))),
|
||||
("Fire Temple GS Scarecrow Climb", ("GS Token", 0x04, 0x10, None, 'Gold Skulltula Token', ("Fire Temple", "Vanilla", "Skulltulas",))),
|
||||
("Fire Temple GS Scarecrow Top", ("GS Token", 0x04, 0x08, None, 'Gold Skulltula Token', ("Fire Temple", "Vanilla", "Skulltulas",))),
|
||||
# Fire Temple MQ
|
||||
("Fire Temple MQ Map Room Side Chest", ("Chest", 0x04, 0x02, None, 'Hylian Shield', ("Fire Temple", "Master Quest",))),
|
||||
("Fire Temple MQ Megaton Hammer Chest", ("Chest", 0x04, 0x00, None, 'Megaton Hammer', ("Fire Temple", "Master Quest",))),
|
||||
("Fire Temple MQ Map Chest", ("Chest", 0x04, 0x0C, None, 'Map (Fire Temple)', ("Fire Temple", "Master Quest",))),
|
||||
("Fire Temple MQ Near Boss Chest", ("Chest", 0x04, 0x07, None, 'Small Key (Fire Temple)', ("Fire Temple", "Master Quest",))),
|
||||
("Fire Temple MQ Big Lava Room Blocked Door Chest", ("Chest", 0x04, 0x01, None, 'Small Key (Fire Temple)', ("Fire Temple", "Master Quest",))),
|
||||
("Fire Temple MQ Boss Key Chest", ("Chest", 0x04, 0x04, None, 'Boss Key (Fire Temple)', ("Fire Temple", "Master Quest",))),
|
||||
("Fire Temple MQ Lizalfos Maze Side Room Chest", ("Chest", 0x04, 0x08, None, 'Small Key (Fire Temple)', ("Fire Temple", "Master Quest",))),
|
||||
("Fire Temple MQ Compass Chest", ("Chest", 0x04, 0x0B, None, 'Compass (Fire Temple)', ("Fire Temple", "Master Quest",))),
|
||||
("Fire Temple MQ Lizalfos Maze Upper Chest", ("Chest", 0x04, 0x06, None, 'Bombs (10)', ("Fire Temple", "Master Quest",))),
|
||||
("Fire Temple MQ Lizalfos Maze Lower Chest", ("Chest", 0x04, 0x03, None, 'Bombs (10)', ("Fire Temple", "Master Quest",))),
|
||||
("Fire Temple MQ Freestanding Key", ("Collectable", 0x04, 0x1C, None, 'Small Key (Fire Temple)', ("Fire Temple", "Master Quest",))),
|
||||
("Fire Temple MQ Chest On Fire", ("Chest", 0x04, 0x05, None, 'Small Key (Fire Temple)', ("Fire Temple", "Master Quest",))),
|
||||
("Fire Temple MQ GS Big Lava Room Open Door", ("GS Token", 0x04, 0x01, None, 'Gold Skulltula Token', ("Fire Temple", "Master Quest", "Skulltulas",))),
|
||||
("Fire Temple MQ GS Skull On Fire", ("GS Token", 0x04, 0x04, None, 'Gold Skulltula Token', ("Fire Temple", "Master Quest", "Skulltulas",))),
|
||||
("Fire Temple MQ GS Fire Wall Maze Center", ("GS Token", 0x04, 0x08, None, 'Gold Skulltula Token', ("Fire Temple", "Master Quest", "Skulltulas",))),
|
||||
("Fire Temple MQ GS Fire Wall Maze Side Room", ("GS Token", 0x04, 0x10, None, 'Gold Skulltula Token', ("Fire Temple", "Master Quest", "Skulltulas",))),
|
||||
("Fire Temple MQ GS Above Fire Wall Maze", ("GS Token", 0x04, 0x02, None, 'Gold Skulltula Token', ("Fire Temple", "Master Quest", "Skulltulas",))),
|
||||
# Fire Temple shared
|
||||
("Fire Temple Volvagia Heart", ("BossHeart", 0x15, 0x4F, None, 'Heart Container', ("Fire Temple", "Vanilla", "Master Quest",))),
|
||||
|
||||
# Water Temple vanilla
|
||||
("Water Temple Compass Chest", ("Chest", 0x05, 0x09, None, 'Compass (Water Temple)', ("Water Temple", "Vanilla",))),
|
||||
("Water Temple Map Chest", ("Chest", 0x05, 0x02, None, 'Map (Water Temple)', ("Water Temple", "Vanilla",))),
|
||||
("Water Temple Cracked Wall Chest", ("Chest", 0x05, 0x00, None, 'Small Key (Water Temple)', ("Water Temple", "Vanilla",))),
|
||||
("Water Temple Torches Chest", ("Chest", 0x05, 0x01, None, 'Small Key (Water Temple)', ("Water Temple", "Vanilla",))),
|
||||
("Water Temple Boss Key Chest", ("Chest", 0x05, 0x05, None, 'Boss Key (Water Temple)', ("Water Temple", "Vanilla",))),
|
||||
("Water Temple Central Pillar Chest", ("Chest", 0x05, 0x06, None, 'Small Key (Water Temple)', ("Water Temple", "Vanilla",))),
|
||||
("Water Temple Central Bow Target Chest", ("Chest", 0x05, 0x08, None, 'Small Key (Water Temple)', ("Water Temple", "Vanilla",))),
|
||||
("Water Temple Longshot Chest", ("Chest", 0x05, 0x07, None, 'Progressive Hookshot', ("Water Temple", "Vanilla",))),
|
||||
("Water Temple River Chest", ("Chest", 0x05, 0x03, None, 'Small Key (Water Temple)', ("Water Temple", "Vanilla",))),
|
||||
("Water Temple Dragon Chest", ("Chest", 0x05, 0x0A, None, 'Small Key (Water Temple)', ("Water Temple", "Vanilla",))),
|
||||
("Water Temple GS Behind Gate", ("GS Token", 0x05, 0x01, None, 'Gold Skulltula Token', ("Water Temple", "Vanilla", "Skulltulas",))),
|
||||
("Water Temple GS Near Boss Key Chest", ("GS Token", 0x05, 0x08, None, 'Gold Skulltula Token', ("Water Temple", "Vanilla", "Skulltulas",))),
|
||||
("Water Temple GS Central Pillar", ("GS Token", 0x05, 0x04, None, 'Gold Skulltula Token', ("Water Temple", "Vanilla", "Skulltulas",))),
|
||||
("Water Temple GS Falling Platform Room", ("GS Token", 0x05, 0x02, None, 'Gold Skulltula Token', ("Water Temple", "Vanilla", "Skulltulas",))),
|
||||
("Water Temple GS River", ("GS Token", 0x05, 0x10, None, 'Gold Skulltula Token', ("Water Temple", "Vanilla", "Skulltulas",))),
|
||||
# Water Temple MQ
|
||||
("Water Temple MQ Longshot Chest", ("Chest", 0x05, 0x00, None, 'Progressive Hookshot', ("Water Temple", "Master Quest",))),
|
||||
("Water Temple MQ Map Chest", ("Chest", 0x05, 0x02, None, 'Map (Water Temple)', ("Water Temple", "Master Quest",))),
|
||||
("Water Temple MQ Compass Chest", ("Chest", 0x05, 0x01, None, 'Compass (Water Temple)', ("Water Temple", "Master Quest",))),
|
||||
("Water Temple MQ Central Pillar Chest", ("Chest", 0x05, 0x06, None, 'Small Key (Water Temple)', ("Water Temple", "Master Quest",))),
|
||||
("Water Temple MQ Boss Key Chest", ("Chest", 0x05, 0x05, None, 'Boss Key (Water Temple)', ("Water Temple", "Master Quest",))),
|
||||
("Water Temple MQ Freestanding Key", ("Collectable", 0x05, 0x01, None, 'Small Key (Water Temple)', ("Water Temple", "Master Quest",))),
|
||||
("Water Temple MQ GS Lizalfos Hallway", ("GS Token", 0x05, 0x01, None, 'Gold Skulltula Token', ("Water Temple", "Master Quest", "Skulltulas",))),
|
||||
("Water Temple MQ GS Before Upper Water Switch", ("GS Token", 0x05, 0x04, None, 'Gold Skulltula Token', ("Water Temple", "Master Quest", "Skulltulas",))),
|
||||
("Water Temple MQ GS River", ("GS Token", 0x05, 0x02, None, 'Gold Skulltula Token', ("Water Temple", "Master Quest", "Skulltulas",))),
|
||||
("Water Temple MQ GS Freestanding Key Area", ("GS Token", 0x05, 0x08, None, 'Gold Skulltula Token', ("Water Temple", "Master Quest", "Skulltulas",))),
|
||||
("Water Temple MQ GS Triple Wall Torch", ("GS Token", 0x05, 0x10, None, 'Gold Skulltula Token', ("Water Temple", "Master Quest", "Skulltulas",))),
|
||||
# Water Temple shared
|
||||
("Water Temple Morpha Heart", ("BossHeart", 0x16, 0x4F, None, 'Heart Container', ("Water Temple", "Vanilla", "Master Quest",))),
|
||||
|
||||
# Shadow Temple vanilla
|
||||
("Shadow Temple Map Chest", ("Chest", 0x07, 0x01, None, 'Map (Shadow Temple)', ("Shadow Temple", "Vanilla",))),
|
||||
("Shadow Temple Hover Boots Chest", ("Chest", 0x07, 0x07, None, 'Hover Boots', ("Shadow Temple", "Vanilla",))),
|
||||
("Shadow Temple Compass Chest", ("Chest", 0x07, 0x03, None, 'Compass (Shadow Temple)', ("Shadow Temple", "Vanilla",))),
|
||||
("Shadow Temple Early Silver Rupee Chest", ("Chest", 0x07, 0x02, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Vanilla",))),
|
||||
("Shadow Temple Invisible Blades Visible Chest", ("Chest", 0x07, 0x0C, None, 'Rupees (5)', ("Shadow Temple", "Vanilla",))),
|
||||
("Shadow Temple Invisible Blades Invisible Chest", ("Chest", 0x07, 0x16, None, 'Arrows (30)', ("Shadow Temple", "Vanilla",))),
|
||||
("Shadow Temple Falling Spikes Lower Chest", ("Chest", 0x07, 0x05, None, 'Arrows (10)', ("Shadow Temple", "Vanilla",))),
|
||||
("Shadow Temple Falling Spikes Upper Chest", ("Chest", 0x07, 0x06, None, 'Rupees (5)', ("Shadow Temple", "Vanilla",))),
|
||||
("Shadow Temple Falling Spikes Switch Chest", ("Chest", 0x07, 0x04, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Vanilla",))),
|
||||
("Shadow Temple Invisible Spikes Chest", ("Chest", 0x07, 0x09, None, 'Rupees (5)', ("Shadow Temple", "Vanilla",))),
|
||||
("Shadow Temple Freestanding Key", ("Collectable", 0x07, 0x01, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Vanilla",))),
|
||||
("Shadow Temple Wind Hint Chest", ("Chest", 0x07, 0x15, None, 'Arrows (10)', ("Shadow Temple", "Vanilla",))),
|
||||
("Shadow Temple After Wind Enemy Chest", ("Chest", 0x07, 0x08, None, 'Rupees (5)', ("Shadow Temple", "Vanilla",))),
|
||||
("Shadow Temple After Wind Hidden Chest", ("Chest", 0x07, 0x14, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Vanilla",))),
|
||||
("Shadow Temple Spike Walls Left Chest", ("Chest", 0x07, 0x0A, None, 'Rupees (5)', ("Shadow Temple", "Vanilla",))),
|
||||
("Shadow Temple Boss Key Chest", ("Chest", 0x07, 0x0B, None, 'Boss Key (Shadow Temple)', ("Shadow Temple", "Vanilla",))),
|
||||
("Shadow Temple Invisible Floormaster Chest", ("Chest", 0x07, 0x0D, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Vanilla",))),
|
||||
("Shadow Temple GS Like Like Room", ("GS Token", 0x07, 0x08, None, 'Gold Skulltula Token', ("Shadow Temple", "Vanilla", "Skulltulas",))),
|
||||
("Shadow Temple GS Falling Spikes Room", ("GS Token", 0x07, 0x02, None, 'Gold Skulltula Token', ("Shadow Temple", "Vanilla", "Skulltulas",))),
|
||||
("Shadow Temple GS Single Giant Pot", ("GS Token", 0x07, 0x01, None, 'Gold Skulltula Token', ("Shadow Temple", "Vanilla", "Skulltulas",))),
|
||||
("Shadow Temple GS Near Ship", ("GS Token", 0x07, 0x10, None, 'Gold Skulltula Token', ("Shadow Temple", "Vanilla", "Skulltulas",))),
|
||||
("Shadow Temple GS Triple Giant Pot", ("GS Token", 0x07, 0x04, None, 'Gold Skulltula Token', ("Shadow Temple", "Vanilla", "Skulltulas",))),
|
||||
# Shadow Temple MQ
|
||||
("Shadow Temple MQ Early Gibdos Chest", ("Chest", 0x07, 0x03, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ Map Chest", ("Chest", 0x07, 0x02, None, 'Map (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ Near Ship Invisible Chest", ("Chest", 0x07, 0x0E, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ Compass Chest", ("Chest", 0x07, 0x01, None, 'Compass (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ Hover Boots Chest", ("Chest", 0x07, 0x07, None, 'Hover Boots', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ Invisible Blades Invisible Chest", ("Chest", 0x07, 0x16, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ Invisible Blades Visible Chest", ("Chest", 0x07, 0x0C, None, 'Rupees (5)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ Beamos Silver Rupees Chest", ("Chest", 0x07, 0x0F, None, 'Arrows (5)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ Falling Spikes Lower Chest", ("Chest", 0x07, 0x05, None, 'Arrows (10)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ Falling Spikes Upper Chest", ("Chest", 0x07, 0x06, None, 'Rupees (5)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ Falling Spikes Switch Chest", ("Chest", 0x07, 0x04, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ Invisible Spikes Chest", ("Chest", 0x07, 0x09, None, 'Rupees (5)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ Stalfos Room Chest", ("Chest", 0x07, 0x10, None, 'Rupees (20)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ Wind Hint Chest", ("Chest", 0x07, 0x15, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ After Wind Hidden Chest", ("Chest", 0x07, 0x14, None, 'Arrows (5)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ After Wind Enemy Chest", ("Chest", 0x07, 0x08, None, 'Rupees (5)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ Boss Key Chest", ("Chest", 0x07, 0x0B, None, 'Boss Key (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ Spike Walls Left Chest", ("Chest", 0x07, 0x0A, None, 'Rupees (5)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ Freestanding Key", ("Collectable", 0x07, 0x06, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ Bomb Flower Chest", ("Chest", 0x07, 0x0D, None, 'Arrows (10)', ("Shadow Temple", "Master Quest",))),
|
||||
("Shadow Temple MQ GS Falling Spikes Room", ("GS Token", 0x07, 0x02, None, 'Gold Skulltula Token', ("Shadow Temple", "Master Quest", "Skulltulas",))),
|
||||
("Shadow Temple MQ GS Wind Hint Room", ("GS Token", 0x07, 0x01, None, 'Gold Skulltula Token', ("Shadow Temple", "Master Quest", "Skulltulas",))),
|
||||
("Shadow Temple MQ GS After Wind", ("GS Token", 0x07, 0x08, None, 'Gold Skulltula Token', ("Shadow Temple", "Master Quest", "Skulltulas",))),
|
||||
("Shadow Temple MQ GS After Ship", ("GS Token", 0x07, 0x10, None, 'Gold Skulltula Token', ("Shadow Temple", "Master Quest", "Skulltulas",))),
|
||||
("Shadow Temple MQ GS Near Boss", ("GS Token", 0x07, 0x04, None, 'Gold Skulltula Token', ("Shadow Temple", "Master Quest", "Skulltulas",))),
|
||||
# Shadow Temple shared
|
||||
("Shadow Temple Bongo Bongo Heart", ("BossHeart", 0x18, 0x4F, None, 'Heart Container', ("Shadow Temple", "Vanilla", "Master Quest",))),
|
||||
|
||||
# Spirit Temple shared
|
||||
# Vanilla and MQ locations are mixed to ensure the positions of Silver Gauntlets/Mirror Shield chests are correct for both versions
|
||||
("Spirit Temple Child Bridge Chest", ("Chest", 0x06, 0x08, None, 'Deku Shield', ("Spirit Temple", "Vanilla",))),
|
||||
("Spirit Temple Child Early Torches Chest", ("Chest", 0x06, 0x00, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Vanilla",))),
|
||||
("Spirit Temple Child Climb North Chest", ("Chest", 0x06, 0x06, None, 'Bombchus (10)', ("Spirit Temple", "Vanilla",))),
|
||||
("Spirit Temple Child Climb East Chest", ("Chest", 0x06, 0x0C, None, 'Deku Shield', ("Spirit Temple", "Vanilla",))),
|
||||
("Spirit Temple Map Chest", ("Chest", 0x06, 0x03, None, 'Map (Spirit Temple)', ("Spirit Temple", "Vanilla",))),
|
||||
("Spirit Temple Sun Block Room Chest", ("Chest", 0x06, 0x01, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Vanilla",))),
|
||||
("Spirit Temple MQ Entrance Front Left Chest", ("Chest", 0x06, 0x1A, None, 'Bombchus (10)', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple MQ Entrance Back Right Chest", ("Chest", 0x06, 0x1F, None, 'Bombchus (10)', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple MQ Entrance Front Right Chest", ("Chest", 0x06, 0x1B, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple MQ Entrance Back Left Chest", ("Chest", 0x06, 0x1E, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple MQ Map Chest", ("Chest", 0x06, 0x00, None, 'Map (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple MQ Map Room Enemy Chest", ("Chest", 0x06, 0x08, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple MQ Child Climb North Chest", ("Chest", 0x06, 0x06, None, 'Bombchus (10)', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple MQ Child Climb South Chest", ("Chest", 0x06, 0x0C, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple MQ Compass Chest", ("Chest", 0x06, 0x03, None, 'Compass (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple MQ Silver Block Hallway Chest", ("Chest", 0x06, 0x1C, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple MQ Sun Block Room Chest", ("Chest", 0x06, 0x01, None, 'Recovery Heart', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple Silver Gauntlets Chest", ("Chest", 0x5C, 0x0B, None, 'Progressive Strength Upgrade', ("Spirit Temple", "Vanilla", "Master Quest", "Desert Colossus"))),
|
||||
|
||||
("Spirit Temple Compass Chest", ("Chest", 0x06, 0x04, None, 'Compass (Spirit Temple)', ("Spirit Temple", "Vanilla",))),
|
||||
("Spirit Temple Early Adult Right Chest", ("Chest", 0x06, 0x07, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Vanilla",))),
|
||||
("Spirit Temple First Mirror Left Chest", ("Chest", 0x06, 0x0D, None, 'Ice Trap', ("Spirit Temple", "Vanilla",))),
|
||||
("Spirit Temple First Mirror Right Chest", ("Chest", 0x06, 0x0E, None, 'Recovery Heart', ("Spirit Temple", "Vanilla",))),
|
||||
("Spirit Temple Statue Room Northeast Chest", ("Chest", 0x06, 0x0F, None, 'Rupees (5)', ("Spirit Temple", "Vanilla",))),
|
||||
("Spirit Temple Statue Room Hand Chest", ("Chest", 0x06, 0x02, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Vanilla",))),
|
||||
("Spirit Temple Near Four Armos Chest", ("Chest", 0x06, 0x05, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Vanilla",))),
|
||||
("Spirit Temple Hallway Right Invisible Chest", ("Chest", 0x06, 0x14, None, 'Recovery Heart', ("Spirit Temple", "Vanilla",))),
|
||||
("Spirit Temple Hallway Left Invisible Chest", ("Chest", 0x06, 0x15, None, 'Recovery Heart', ("Spirit Temple", "Vanilla",))),
|
||||
("Spirit Temple MQ Child Hammer Switch Chest", ("Chest", 0x06, 0x1D, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple MQ Statue Room Lullaby Chest", ("Chest", 0x06, 0x0F, None, 'Rupees (5)', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple MQ Statue Room Invisible Chest", ("Chest", 0x06, 0x02, None, 'Recovery Heart', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple MQ Leever Room Chest", ("Chest", 0x06, 0x04, None, 'Rupees (50)', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple MQ Symphony Room Chest", ("Chest", 0x06, 0x07, None, 'Rupees (50)', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple MQ Beamos Room Chest", ("Chest", 0x06, 0x19, None, 'Recovery Heart', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple MQ Chest Switch Chest", ("Chest", 0x06, 0x18, None, 'Ice Trap', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple MQ Boss Key Chest", ("Chest", 0x06, 0x05, None, 'Boss Key (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
|
||||
("Spirit Temple Mirror Shield Chest", ("Chest", 0x5C, 0x09, None, 'Mirror Shield', ("Spirit Temple", "Vanilla", "Master Quest", "Desert Colossus"))),
|
||||
|
||||
("Spirit Temple Boss Key Chest", ("Chest", 0x06, 0x0A, None, 'Boss Key (Spirit Temple)', ("Spirit Temple", "Vanilla",))),
|
||||
("Spirit Temple Topmost Chest", ("Chest", 0x06, 0x12, None, 'Bombs (20)', ("Spirit Temple", "Vanilla",))),
|
||||
("Spirit Temple MQ Mirror Puzzle Invisible Chest", ("Chest", 0x06, 0x12, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
|
||||
|
||||
("Spirit Temple GS Metal Fence", ("GS Token", 0x06, 0x10, None, 'Gold Skulltula Token', ("Spirit Temple", "Vanilla", "Skulltulas",))),
|
||||
("Spirit Temple GS Sun on Floor Room", ("GS Token", 0x06, 0x08, None, 'Gold Skulltula Token', ("Spirit Temple", "Vanilla", "Skulltulas",))),
|
||||
("Spirit Temple GS Hall After Sun Block Room", ("GS Token", 0x06, 0x01, None, 'Gold Skulltula Token', ("Spirit Temple", "Vanilla", "Skulltulas",))),
|
||||
("Spirit Temple GS Lobby", ("GS Token", 0x06, 0x04, None, 'Gold Skulltula Token', ("Spirit Temple", "Vanilla", "Skulltulas",))),
|
||||
("Spirit Temple GS Boulder Room", ("GS Token", 0x06, 0x02, None, 'Gold Skulltula Token', ("Spirit Temple", "Vanilla", "Skulltulas",))),
|
||||
("Spirit Temple MQ GS Sun Block Room", ("GS Token", 0x06, 0x01, None, 'Gold Skulltula Token', ("Spirit Temple", "Master Quest", "Skulltulas",))),
|
||||
("Spirit Temple MQ GS Leever Room", ("GS Token", 0x06, 0x02, None, 'Gold Skulltula Token', ("Spirit Temple", "Master Quest", "Skulltulas",))),
|
||||
("Spirit Temple MQ GS Symphony Room", ("GS Token", 0x06, 0x08, None, 'Gold Skulltula Token', ("Spirit Temple", "Master Quest", "Skulltulas",))),
|
||||
("Spirit Temple MQ GS Nine Thrones Room West", ("GS Token", 0x06, 0x04, None, 'Gold Skulltula Token', ("Spirit Temple", "Master Quest", "Skulltulas",))),
|
||||
("Spirit Temple MQ GS Nine Thrones Room North", ("GS Token", 0x06, 0x10, None, 'Gold Skulltula Token', ("Spirit Temple", "Master Quest", "Skulltulas",))),
|
||||
|
||||
("Spirit Temple Twinrova Heart", ("BossHeart", 0x17, 0x4F, None, 'Heart Container', ("Spirit Temple", "Vanilla", "Master Quest",))),
|
||||
|
||||
# Ice Cavern vanilla
|
||||
("Ice Cavern Map Chest", ("Chest", 0x09, 0x00, None, 'Map (Ice Cavern)', ("Ice Cavern", "Vanilla",))),
|
||||
("Ice Cavern Compass Chest", ("Chest", 0x09, 0x01, None, 'Compass (Ice Cavern)', ("Ice Cavern", "Vanilla",))),
|
||||
("Ice Cavern Freestanding PoH", ("Collectable", 0x09, 0x01, None, 'Piece of Heart', ("Ice Cavern", "Vanilla",))),
|
||||
("Ice Cavern Iron Boots Chest", ("Chest", 0x09, 0x02, None, 'Iron Boots', ("Ice Cavern", "Vanilla",))),
|
||||
("Ice Cavern GS Spinning Scythe Room", ("GS Token", 0x09, 0x02, None, 'Gold Skulltula Token', ("Ice Cavern", "Vanilla", "Skulltulas",))),
|
||||
("Ice Cavern GS Heart Piece Room", ("GS Token", 0x09, 0x04, None, 'Gold Skulltula Token', ("Ice Cavern", "Vanilla", "Skulltulas",))),
|
||||
("Ice Cavern GS Push Block Room", ("GS Token", 0x09, 0x01, None, 'Gold Skulltula Token', ("Ice Cavern", "Vanilla", "Skulltulas",))),
|
||||
# Ice Cavern MQ
|
||||
("Ice Cavern MQ Map Chest", ("Chest", 0x09, 0x01, None, 'Map (Ice Cavern)', ("Ice Cavern", "Master Quest",))),
|
||||
("Ice Cavern MQ Compass Chest", ("Chest", 0x09, 0x00, None, 'Compass (Ice Cavern)', ("Ice Cavern", "Master Quest",))),
|
||||
("Ice Cavern MQ Freestanding PoH", ("Collectable", 0x09, 0x01, None, 'Piece of Heart', ("Ice Cavern", "Master Quest",))),
|
||||
("Ice Cavern MQ Iron Boots Chest", ("Chest", 0x09, 0x02, None, 'Iron Boots', ("Ice Cavern", "Master Quest",))),
|
||||
("Ice Cavern MQ GS Red Ice", ("GS Token", 0x09, 0x02, None, 'Gold Skulltula Token', ("Ice Cavern", "Master Quest", "Skulltulas",))),
|
||||
("Ice Cavern MQ GS Ice Block", ("GS Token", 0x09, 0x04, None, 'Gold Skulltula Token', ("Ice Cavern", "Master Quest", "Skulltulas",))),
|
||||
("Ice Cavern MQ GS Scarecrow", ("GS Token", 0x09, 0x01, None, 'Gold Skulltula Token', ("Ice Cavern", "Master Quest", "Skulltulas",))),
|
||||
|
||||
# Gerudo Training Grounds vanilla
|
||||
("Gerudo Training Grounds Lobby Left Chest", ("Chest", 0x0B, 0x13, None, 'Rupees (5)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Lobby Right Chest", ("Chest", 0x0B, 0x07, None, 'Arrows (10)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Stalfos Chest", ("Chest", 0x0B, 0x00, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Before Heavy Block Chest", ("Chest", 0x0B, 0x11, None, 'Arrows (30)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Heavy Block First Chest", ("Chest", 0x0B, 0x0F, None, 'Rupees (200)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Heavy Block Second Chest", ("Chest", 0x0B, 0x0E, None, 'Rupees (5)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Heavy Block Third Chest", ("Chest", 0x0B, 0x14, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Heavy Block Fourth Chest", ("Chest", 0x0B, 0x02, None, 'Ice Trap', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Eye Statue Chest", ("Chest", 0x0B, 0x03, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Near Scarecrow Chest", ("Chest", 0x0B, 0x04, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Hammer Room Clear Chest", ("Chest", 0x0B, 0x12, None, 'Arrows (10)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Hammer Room Switch Chest", ("Chest", 0x0B, 0x10, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Freestanding Key", ("Collectable", 0x0B, 0x01, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Maze Right Central Chest", ("Chest", 0x0B, 0x05, None, 'Bombchus (5)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Maze Right Side Chest", ("Chest", 0x0B, 0x08, None, 'Arrows (30)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Underwater Silver Rupee Chest", ("Chest", 0x0B, 0x0D, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Beamos Chest", ("Chest", 0x0B, 0x01, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Hidden Ceiling Chest", ("Chest", 0x0B, 0x0B, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Maze Path First Chest", ("Chest", 0x0B, 0x06, None, 'Rupees (50)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Maze Path Second Chest", ("Chest", 0x0B, 0x0A, None, 'Rupees (20)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Maze Path Third Chest", ("Chest", 0x0B, 0x09, None, 'Arrows (30)', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
("Gerudo Training Grounds Maze Path Final Chest", ("Chest", 0x0B, 0x0C, None, 'Ice Arrows', ("Gerudo Training Grounds", "Vanilla",))),
|
||||
# Gerudo Training Grounds MQ
|
||||
("Gerudo Training Grounds MQ Lobby Left Chest", ("Chest", 0x0B, 0x13, None, 'Arrows (10)', ("Gerudo Training Grounds", "Master Quest",))),
|
||||
("Gerudo Training Grounds MQ Lobby Right Chest", ("Chest", 0x0B, 0x07, None, 'Bombchus (5)', ("Gerudo Training Grounds", "Master Quest",))),
|
||||
("Gerudo Training Grounds MQ First Iron Knuckle Chest", ("Chest", 0x0B, 0x00, None, 'Rupees (5)', ("Gerudo Training Grounds", "Master Quest",))),
|
||||
("Gerudo Training Grounds MQ Before Heavy Block Chest", ("Chest", 0x0B, 0x11, None, 'Arrows (10)', ("Gerudo Training Grounds", "Master Quest",))),
|
||||
("Gerudo Training Grounds MQ Heavy Block Chest", ("Chest", 0x0B, 0x02, None, 'Rupees (50)', ("Gerudo Training Grounds", "Master Quest",))),
|
||||
("Gerudo Training Grounds MQ Eye Statue Chest", ("Chest", 0x0B, 0x03, None, 'Bombchus (10)', ("Gerudo Training Grounds", "Master Quest",))),
|
||||
("Gerudo Training Grounds MQ Ice Arrows Chest", ("Chest", 0x0B, 0x04, None, 'Ice Arrows', ("Gerudo Training Grounds", "Master Quest",))),
|
||||
("Gerudo Training Grounds MQ Second Iron Knuckle Chest", ("Chest", 0x0B, 0x12, None, 'Arrows (10)', ("Gerudo Training Grounds", "Master Quest",))),
|
||||
("Gerudo Training Grounds MQ Flame Circle Chest", ("Chest", 0x0B, 0x0E, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Master Quest",))),
|
||||
("Gerudo Training Grounds MQ Maze Right Central Chest", ("Chest", 0x0B, 0x05, None, 'Rupees (5)', ("Gerudo Training Grounds", "Master Quest",))),
|
||||
("Gerudo Training Grounds MQ Maze Right Side Chest", ("Chest", 0x0B, 0x08, None, 'Rupee (Treasure Chest Game)', ("Gerudo Training Grounds", "Master Quest",))),
|
||||
("Gerudo Training Grounds MQ Underwater Silver Rupee Chest", ("Chest", 0x0B, 0x0D, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Master Quest",))),
|
||||
("Gerudo Training Grounds MQ Dinolfos Chest", ("Chest", 0x0B, 0x01, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Master Quest",))),
|
||||
("Gerudo Training Grounds MQ Hidden Ceiling Chest", ("Chest", 0x0B, 0x0B, None, 'Rupees (50)', ("Gerudo Training Grounds", "Master Quest",))),
|
||||
("Gerudo Training Grounds MQ Maze Path First Chest", ("Chest", 0x0B, 0x06, None, 'Rupee (1)', ("Gerudo Training Grounds", "Master Quest",))),
|
||||
("Gerudo Training Grounds MQ Maze Path Third Chest", ("Chest", 0x0B, 0x09, None, 'Rupee (Treasure Chest Game)', ("Gerudo Training Grounds", "Master Quest",))),
|
||||
("Gerudo Training Grounds MQ Maze Path Second Chest", ("Chest", 0x0B, 0x0A, None, 'Rupees (20)', ("Gerudo Training Grounds", "Master Quest",))),
|
||||
|
||||
# Ganon's Castle vanilla
|
||||
("Ganons Castle Forest Trial Chest", ("Chest", 0x0D, 0x09, None, 'Rupees (5)', ("Ganon's Castle", "Vanilla",))),
|
||||
("Ganons Castle Water Trial Left Chest", ("Chest", 0x0D, 0x07, None, 'Ice Trap', ("Ganon's Castle", "Vanilla",))),
|
||||
("Ganons Castle Water Trial Right Chest", ("Chest", 0x0D, 0x06, None, 'Recovery Heart', ("Ganon's Castle", "Vanilla",))),
|
||||
("Ganons Castle Shadow Trial Front Chest", ("Chest", 0x0D, 0x08, None, 'Rupees (5)', ("Ganon's Castle", "Vanilla",))),
|
||||
("Ganons Castle Shadow Trial Golden Gauntlets Chest", ("Chest", 0x0D, 0x05, None, 'Progressive Strength Upgrade', ("Ganon's Castle", "Vanilla",))),
|
||||
("Ganons Castle Light Trial First Left Chest", ("Chest", 0x0D, 0x0C, None, 'Rupees (5)', ("Ganon's Castle", "Vanilla",))),
|
||||
("Ganons Castle Light Trial Second Left Chest", ("Chest", 0x0D, 0x0B, None, 'Ice Trap', ("Ganon's Castle", "Vanilla",))),
|
||||
("Ganons Castle Light Trial Third Left Chest", ("Chest", 0x0D, 0x0D, None, 'Recovery Heart', ("Ganon's Castle", "Vanilla",))),
|
||||
("Ganons Castle Light Trial First Right Chest", ("Chest", 0x0D, 0x0E, None, 'Ice Trap', ("Ganon's Castle", "Vanilla",))),
|
||||
("Ganons Castle Light Trial Second Right Chest", ("Chest", 0x0D, 0x0A, None, 'Arrows (30)', ("Ganon's Castle", "Vanilla",))),
|
||||
("Ganons Castle Light Trial Third Right Chest", ("Chest", 0x0D, 0x0F, None, 'Ice Trap', ("Ganon's Castle", "Vanilla",))),
|
||||
("Ganons Castle Light Trial Invisible Enemies Chest", ("Chest", 0x0D, 0x10, None, 'Small Key (Ganons Castle)', ("Ganon's Castle", "Vanilla",))),
|
||||
("Ganons Castle Light Trial Lullaby Chest", ("Chest", 0x0D, 0x11, None, 'Small Key (Ganons Castle)', ("Ganon's Castle", "Vanilla",))),
|
||||
("Ganons Castle Spirit Trial Crystal Switch Chest", ("Chest", 0x0D, 0x12, None, 'Bombchus (20)', ("Ganon's Castle", "Vanilla",))),
|
||||
("Ganons Castle Spirit Trial Invisible Chest", ("Chest", 0x0D, 0x14, None, 'Arrows (10)', ("Ganon's Castle", "Vanilla",))),
|
||||
("Ganons Castle Deku Scrub Left", ("NPC", 0x0D, 0x3A, None, 'Buy Green Potion', ("Ganon's Castle", "Vanilla", "Deku Scrub",))),
|
||||
("Ganons Castle Deku Scrub Center-Left", ("NPC", 0x0D, 0x37, None, 'Buy Bombs (5) [35]', ("Ganon's Castle", "Vanilla", "Deku Scrub",))),
|
||||
("Ganons Castle Deku Scrub Center-Right", ("NPC", 0x0D, 0x33, None, 'Buy Arrows (30)', ("Ganon's Castle", "Vanilla", "Deku Scrub",))),
|
||||
("Ganons Castle Deku Scrub Right", ("NPC", 0x0D, 0x39, None, 'Buy Red Potion [30]', ("Ganon's Castle", "Vanilla", "Deku Scrub",))),
|
||||
# Ganon's Castle MQ
|
||||
("Ganons Castle MQ Forest Trial Freestanding Key", ("Collectable", 0x0D, 0x01, None, 'Small Key (Ganons Castle)', ("Ganon's Castle", "Master Quest",))),
|
||||
("Ganons Castle MQ Forest Trial Eye Switch Chest", ("Chest", 0x0D, 0x02, None, 'Arrows (10)', ("Ganon's Castle", "Master Quest",))),
|
||||
("Ganons Castle MQ Forest Trial Frozen Eye Switch Chest", ("Chest", 0x0D, 0x03, None, 'Bombs (5)', ("Ganon's Castle", "Master Quest",))),
|
||||
("Ganons Castle MQ Water Trial Chest", ("Chest", 0x0D, 0x01, None, 'Rupees (20)', ("Ganon's Castle", "Master Quest",))),
|
||||
("Ganons Castle MQ Shadow Trial Bomb Flower Chest", ("Chest", 0x0D, 0x00, None, 'Arrows (10)', ("Ganon's Castle", "Master Quest",))),
|
||||
("Ganons Castle MQ Shadow Trial Eye Switch Chest", ("Chest", 0x0D, 0x05, None, 'Small Key (Ganons Castle)', ("Ganon's Castle", "Master Quest",))),
|
||||
("Ganons Castle MQ Light Trial Lullaby Chest", ("Chest", 0x0D, 0x04, None, 'Recovery Heart', ("Ganon's Castle", "Master Quest",))),
|
||||
("Ganons Castle MQ Spirit Trial First Chest", ("Chest", 0x0D, 0x0A, None, 'Bombchus (10)', ("Ganon's Castle", "Master Quest",))),
|
||||
("Ganons Castle MQ Spirit Trial Invisible Chest", ("Chest", 0x0D, 0x14, None, 'Arrows (10)', ("Ganon's Castle", "Master Quest",))),
|
||||
("Ganons Castle MQ Spirit Trial Sun Front Left Chest", ("Chest", 0x0D, 0x09, None, 'Recovery Heart', ("Ganon's Castle", "Master Quest",))),
|
||||
("Ganons Castle MQ Spirit Trial Sun Back Left Chest", ("Chest", 0x0D, 0x08, None, 'Small Key (Ganons Castle)', ("Ganon's Castle", "Master Quest",))),
|
||||
("Ganons Castle MQ Spirit Trial Sun Back Right Chest", ("Chest", 0x0D, 0x07, None, 'Recovery Heart', ("Ganon's Castle", "Master Quest",))),
|
||||
("Ganons Castle MQ Spirit Trial Golden Gauntlets Chest", ("Chest", 0x0D, 0x06, None, 'Progressive Strength Upgrade', ("Ganon's Castle", "Master Quest",))),
|
||||
("Ganons Castle MQ Deku Scrub Left", ("NPC", 0x0D, 0x3A, None, 'Buy Green Potion', ("Ganon's Castle", "Master Quest", "Deku Scrub",))),
|
||||
("Ganons Castle MQ Deku Scrub Center-Left", ("NPC", 0x0D, 0x37, None, 'Buy Bombs (5) [35]', ("Ganon's Castle", "Master Quest", "Deku Scrub",))),
|
||||
("Ganons Castle MQ Deku Scrub Center", ("NPC", 0x0D, 0x33, None, 'Buy Arrows (30)', ("Ganon's Castle", "Master Quest", "Deku Scrub",))),
|
||||
("Ganons Castle MQ Deku Scrub Center-Right", ("NPC", 0x0D, 0x39, None, 'Buy Red Potion [30]', ("Ganon's Castle", "Master Quest", "Deku Scrub",))),
|
||||
("Ganons Castle MQ Deku Scrub Right", ("NPC", 0x0D, 0x30, None, 'Buy Deku Nut (5)', ("Ganon's Castle", "Master Quest", "Deku Scrub",))),
|
||||
# Ganon's Castle shared
|
||||
("Ganons Tower Boss Key Chest", ("Chest", 0x0A, 0x0B, None, 'Boss Key (Ganons Castle)', ("Ganon's Castle", "Vanilla", "Master Quest",))),
|
||||
|
||||
## Events and Drops
|
||||
("Pierre", ("Event", None, None, None, 'Scarecrow Song', None)),
|
||||
("Deliver Rutos Letter", ("Event", None, None, None, 'Deliver Letter', None)),
|
||||
("Master Sword Pedestal", ("Event", None, None, None, 'Time Travel', None)),
|
||||
|
||||
("Deku Baba Sticks", ("Drop", None, None, None, 'Deku Stick Drop', None)),
|
||||
("Deku Baba Nuts", ("Drop", None, None, None, 'Deku Nut Drop', None)),
|
||||
("Stick Pot", ("Drop", None, None, None, 'Deku Stick Drop', None)),
|
||||
("Nut Pot", ("Drop", None, None, None, 'Deku Nut Drop', None)),
|
||||
("Nut Crate", ("Drop", None, None, None, 'Deku Nut Drop', None)),
|
||||
("Blue Fire", ("Drop", None, None, None, 'Blue Fire', None)),
|
||||
("Lone Fish", ("Drop", None, None, None, 'Fish', None)),
|
||||
("Fish Group", ("Drop", None, None, None, 'Fish', None)),
|
||||
("Bug Rock", ("Drop", None, None, None, 'Bugs', None)),
|
||||
("Bug Shrub", ("Drop", None, None, None, 'Bugs', None)),
|
||||
("Wandering Bugs", ("Drop", None, None, None, 'Bugs', None)),
|
||||
("Fairy Pot", ("Drop", None, None, None, 'Fairy', None)),
|
||||
("Free Fairies", ("Drop", None, None, None, 'Fairy', None)),
|
||||
("Wall Fairy", ("Drop", None, None, None, 'Fairy', None)),
|
||||
("Butterfly Fairy", ("Drop", None, None, None, 'Fairy', None)),
|
||||
("Gossip Stone Fairy", ("Drop", None, None, None, 'Fairy', None)),
|
||||
("Bean Plant Fairy", ("Drop", None, None, None, 'Fairy', None)),
|
||||
("Fairy Pond", ("Drop", None, None, None, 'Fairy', None)),
|
||||
("Big Poe Kill", ("Drop", None, None, None, 'Big Poe', None)),
|
||||
|
||||
## Hints
|
||||
# These are not actual locations, but are filler spots used for hint reachability.
|
||||
# Hint location types must start with 'Hint'.
|
||||
("DMC Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("DMT Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("Colossus Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("Dodongos Cavern Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("GV Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("GC Maze Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("GC Medigoron Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("Graveyard Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("HC Malon Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("HC Rock Wall Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("HC Storms Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("HF Cow Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("KF Deku Tree Gossip Stone (Left)", ("HintStone", None, None, None, None, None)),
|
||||
("KF Deku Tree Gossip Stone (Right)", ("HintStone", None, None, None, None, None)),
|
||||
("KF Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("LH Lab Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("LH Gossip Stone (Southeast)", ("HintStone", None, None, None, None, None)),
|
||||
("LH Gossip Stone (Southwest)", ("HintStone", None, None, None, None, None)),
|
||||
("LW Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("SFM Maze Gossip Stone (Lower)", ("HintStone", None, None, None, None, None)),
|
||||
("SFM Maze Gossip Stone (Upper)", ("HintStone", None, None, None, None, None)),
|
||||
("SFM Saria Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("ToT Gossip Stone (Left)", ("HintStone", None, None, None, None, None)),
|
||||
("ToT Gossip Stone (Left-Center)", ("HintStone", None, None, None, None, None)),
|
||||
("ToT Gossip Stone (Right)", ("HintStone", None, None, None, None, None)),
|
||||
("ToT Gossip Stone (Right-Center)", ("HintStone", None, None, None, None, None)),
|
||||
("ZD Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("ZF Fairy Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("ZF Jabu Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("ZR Near Grottos Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("ZR Near Domain Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
|
||||
("HF Near Market Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("HF Southeast Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("HF Open Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("Kak Open Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("ZR Open Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("KF Storms Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("LW Near Shortcuts Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("DMT Storms Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
("DMC Upper Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
|
||||
|
||||
("Ganondorf Hint", ("Hint", None, None, None, None, None)),
|
||||
])
|
||||
|
||||
location_sort_order = {
|
||||
loc: i for i, loc in enumerate(location_table.keys())
|
||||
}
|
||||
|
||||
# Business Scrub Details
|
||||
business_scrubs = [
|
||||
# id price text text replacement
|
||||
(0x30, 20, 0x10A0, ["Deku Nuts", "a \x05\x42mysterious item\x05\x40"]),
|
||||
(0x31, 15, 0x10A1, ["Deku Sticks", "a \x05\x42mysterious item\x05\x40"]),
|
||||
(0x3E, 10, 0x10A2, ["Piece of Heart", "\x05\x42mysterious item\x05\x40"]),
|
||||
(0x33, 40, 0x10CA, ["\x05\x41Deku Seeds", "a \x05\x42mysterious item"]),
|
||||
(0x34, 50, 0x10CB, ["\x41Deku Shield", "\x42mysterious item"]),
|
||||
(0x37, 40, 0x10CC, ["\x05\x41Bombs", "a \x05\x42mysterious item"]),
|
||||
(0x38, 00, 0x10CD, ["\x05\x41Arrows", "a \x05\x42mysterious item"]), # unused
|
||||
(0x39, 40, 0x10CE, ["\x05\x41Red Potion", "\x05\x42mysterious item"]),
|
||||
(0x3A, 40, 0x10CF, ["Green Potion", "mysterious item"]),
|
||||
(0x77, 40, 0x10DC, ["enable you to pick up more\x01\x05\x41Deku Sticks", "sell you a \x05\x42mysterious item"]),
|
||||
(0x79, 40, 0x10DD, ["enable you to pick up more \x05\x41Deku\x01Nuts", "sell you a \x05\x42mysterious item"]),
|
||||
]
|
||||
|
||||
dungeons = ('Deku Tree', 'Dodongo\'s Cavern', 'Jabu Jabu\'s Belly', 'Forest Temple', 'Fire Temple', 'Water Temple', 'Spirit Temple', 'Shadow Temple', 'Ice Cavern', 'Bottom of the Well', 'Gerudo Training Grounds', 'Ganon\'s Castle')
|
||||
location_groups = {
|
||||
'Song': [name for (name, data) in location_table.items() if data[0] == 'Song'],
|
||||
'Chest': [name for (name, data) in location_table.items() if data[0] == 'Chest'],
|
||||
'Collectable': [name for (name, data) in location_table.items() if data[0] == 'Collectable'],
|
||||
'BossHeart': [name for (name, data) in location_table.items() if data[0] == 'BossHeart'],
|
||||
'CollectableLike': [name for (name, data) in location_table.items() if data[0] in ('Collectable', 'BossHeart', 'GS Token')],
|
||||
'CanSee': [name for (name, data) in location_table.items()
|
||||
if data[0] in ('Collectable', 'BossHeart', 'GS Token', 'Shop')
|
||||
# Treasure Box Shop, Bombchu Bowling, Hyrule Field (OoT), Lake Hylia (RL/FA)
|
||||
or data[0:2] in [('Chest', 0x10), ('NPC', 0x4B), ('NPC', 0x51), ('NPC', 0x57)]],
|
||||
'Dungeon': [name for (name, data) in location_table.items() if data[5] is not None and any(dungeon in data[5] for dungeon in dungeons)],
|
||||
}
|
||||
|
||||
|
||||
def location_is_viewable(loc_name, correct_chest_sizes):
|
||||
return correct_chest_sizes and loc_name in location_groups['Chest'] or loc_name in location_groups['CanSee']
|
||||
|
||||
|
||||
# Function to run exactly once after after placing items in drop locations for each world
|
||||
# Sets all Drop locations to a unique name in order to avoid name issues and to identify locations in the spoiler
|
||||
# Also cause them to not be shown in the list of locations, only in playthrough
|
||||
def set_drop_location_names(ootworld):
|
||||
for region in ootworld.regions:
|
||||
for location in region.locations:
|
||||
if location.type == 'Drop':
|
||||
location.name = location.parent_region.name + " " + location.name
|
||||
location.show_in_spoiler = False
|
||||
1359
worlds/oot/LogicTricks.py
Normal file
1359
worlds/oot/LogicTricks.py
Normal file
File diff suppressed because it is too large
Load Diff
732
worlds/oot/MQ.py
Normal file
732
worlds/oot/MQ.py
Normal file
@@ -0,0 +1,732 @@
|
||||
# mzxrules 2018
|
||||
# In order to patch MQ to the existing data...
|
||||
#
|
||||
# Scenes:
|
||||
#
|
||||
# Ice Cavern (Scene 9) needs to have it's header altered to support MQ's path list. This
|
||||
# expansion will delete the otherwise unused alternate headers command
|
||||
#
|
||||
# Transition actors will be patched over the old data, as the number of records is the same
|
||||
# Path data will be appended to the end of the scene file.
|
||||
#
|
||||
# The size of a single path on file is NUM_POINTS * 6, rounded up to the nearest 4 byte boundary
|
||||
# The total size consumed by the path data is NUM_PATHS * 8, plus the sum of all path file sizes
|
||||
# padded to the nearest 0x10 bytes
|
||||
#
|
||||
# Collision:
|
||||
# OoT's collision data consists of these elements: vertices, surface types, water boxes,
|
||||
# camera behavior data, and polys. MQ's vertice and polygon geometry data is identical.
|
||||
# However, the surface types and the collision exclusion flags bound to the polys have changed
|
||||
# for some polygons, as well as the number of surface type records and camera type records.
|
||||
#
|
||||
# To patch collision, a flag denotes whether collision data cannot be written in place without
|
||||
# expanding the size of the scene file. If true, the camera data is relocated to the end
|
||||
# of the scene file, and the surface types are shifted down into where the camera types
|
||||
# were situated. If false, the camera data isn't moved, but rather the surface type list
|
||||
# will be shifted to the end of the camera data
|
||||
#
|
||||
# Rooms:
|
||||
#
|
||||
# Object file initialization data will be appended to the end of the room file.
|
||||
# The total size consumed by the object file data is NUM_OBJECTS * 0x02, aligned to
|
||||
# the nearest 0x04 bytes
|
||||
#
|
||||
# Actor spawn data will be appended to the end of the room file, after the objects.
|
||||
# The total size consumed by the actor spawn data is NUM_ACTORS * 0x10
|
||||
#
|
||||
# Finally:
|
||||
#
|
||||
# Scene and room files will be padded to the nearest 0x10 bytes
|
||||
#
|
||||
# Maps:
|
||||
# Jabu Jabu's B1 map contains no chests in the vanilla layout. Because of this,
|
||||
# the floor map data is missing a vertex pointer that would point within kaleido_scope.
|
||||
# As such, if the file moves, the patch will break.
|
||||
|
||||
from .Utils import data_path
|
||||
from .Rom import Rom
|
||||
import json
|
||||
from struct import pack, unpack
|
||||
|
||||
SCENE_TABLE = 0xB71440
|
||||
|
||||
|
||||
class File(object):
|
||||
def __init__(self, file):
|
||||
self.name = file['Name']
|
||||
self.start = int(file['Start'], 16) if 'Start' in file else 0
|
||||
self.end = int(file['End'], 16) if 'End' in file else self.start
|
||||
self.remap = file['RemapStart'] if 'RemapStart' in file else None
|
||||
self.from_file = self.start
|
||||
|
||||
# used to update the file's associated dmadata record
|
||||
self.dma_key = self.start
|
||||
|
||||
if self.remap is not None:
|
||||
self.remap = int(self.remap, 16)
|
||||
|
||||
def __repr__(self):
|
||||
remap = "None"
|
||||
if self.remap is not None:
|
||||
remap = "{0:x}".format(self.remap)
|
||||
return "{0}: {1:x} {2:x}, remap {3}".format(self.name, self.start, self.end, remap)
|
||||
|
||||
def relocate(self, rom:Rom):
|
||||
if self.remap is None:
|
||||
self.remap = rom.free_space()
|
||||
|
||||
new_start = self.remap
|
||||
|
||||
offset = new_start - self.start
|
||||
new_end = self.end + offset
|
||||
|
||||
rom.buffer[new_start:new_end] = rom.buffer[self.start:self.end]
|
||||
self.start = new_start
|
||||
self.end = new_end
|
||||
update_dmadata(rom, self)
|
||||
|
||||
# The file will now refer to the new copy of the file
|
||||
def copy(self, rom:Rom):
|
||||
self.dma_key = None
|
||||
self.relocate(rom)
|
||||
|
||||
|
||||
class CollisionMesh(object):
|
||||
def __init__(self, rom:Rom, start, offset):
|
||||
self.offset = offset
|
||||
self.poly_addr = rom.read_int32(start + offset + 0x18)
|
||||
self.polytypes_addr = rom.read_int32(start + offset + 0x1C)
|
||||
self.camera_data_addr = rom.read_int32(start + offset + 0x20)
|
||||
self.polytypes = (self.poly_addr - self.polytypes_addr) // 8
|
||||
|
||||
def write_to_scene(self, rom:Rom, start):
|
||||
addr = start + self.offset + 0x18
|
||||
rom.write_int32s(addr, [self.poly_addr, self.polytypes_addr, self.camera_data_addr])
|
||||
|
||||
|
||||
class ColDelta(object):
|
||||
def __init__(self, delta):
|
||||
self.is_larger = delta['IsLarger']
|
||||
self.polys = delta['Polys']
|
||||
self.polytypes = delta['PolyTypes']
|
||||
self.cams = delta['Cams']
|
||||
|
||||
|
||||
class Icon(object):
|
||||
def __init__(self, data):
|
||||
self.icon = data["Icon"];
|
||||
self.count = data["Count"];
|
||||
self.points = [IconPoint(x) for x in data["IconPoints"]]
|
||||
|
||||
def write_to_minimap(self, rom:Rom, addr):
|
||||
rom.write_sbyte(addr, self.icon)
|
||||
rom.write_byte(addr + 1, self.count)
|
||||
cur = 2
|
||||
for p in self.points:
|
||||
p.write_to_minimap(rom, addr + cur)
|
||||
cur += 0x03
|
||||
|
||||
def write_to_floormap(self, rom:Rom, addr):
|
||||
rom.write_int16(addr, self.icon)
|
||||
rom.write_int32(addr + 0x10, self.count)
|
||||
|
||||
cur = 0x14
|
||||
for p in self.points:
|
||||
p.write_to_floormap(rom, addr + cur)
|
||||
cur += 0x0C
|
||||
|
||||
|
||||
class IconPoint(object):
|
||||
def __init__(self, point):
|
||||
self.flag = point["Flag"]
|
||||
self.x = point["x"]
|
||||
self.y = point["y"]
|
||||
|
||||
def write_to_minimap(self, rom:Rom, addr):
|
||||
rom.write_sbyte(addr, self.flag)
|
||||
rom.write_byte(addr+1, self.x)
|
||||
rom.write_byte(addr+2, self.y)
|
||||
|
||||
def write_to_floormap(self, rom:Rom, addr):
|
||||
rom.write_int16(addr, self.flag)
|
||||
rom.write_f32(addr + 4, float(self.x))
|
||||
rom.write_f32(addr + 8, float(self.y))
|
||||
|
||||
|
||||
class Scene(object):
|
||||
def __init__(self, scene):
|
||||
self.file = File(scene['File'])
|
||||
self.id = scene['Id']
|
||||
self.transition_actors = [convert_actor_data(x) for x in scene['TActors']]
|
||||
self.rooms = [Room(x) for x in scene['Rooms']]
|
||||
self.paths = []
|
||||
self.coldelta = ColDelta(scene["ColDelta"])
|
||||
self.minimaps = [[Icon(icon) for icon in minimap['Icons']] for minimap in scene['Minimaps']]
|
||||
self.floormaps = [[Icon(icon) for icon in floormap['Icons']] for floormap in scene['Floormaps']]
|
||||
temp_paths = scene['Paths']
|
||||
for item in temp_paths:
|
||||
self.paths.append(item['Points'])
|
||||
|
||||
|
||||
def write_data(self, rom:Rom):
|
||||
# write floormap and minimap data
|
||||
self.write_map_data(rom)
|
||||
|
||||
# move file to remap address
|
||||
if self.file.remap is not None:
|
||||
self.file.relocate(rom)
|
||||
|
||||
start = self.file.start
|
||||
headcur = self.file.start
|
||||
|
||||
room_list_offset = 0
|
||||
|
||||
code = rom.read_byte(headcur)
|
||||
loop = 0x20
|
||||
while loop > 0 and code != 0x14: #terminator
|
||||
loop -= 1
|
||||
|
||||
if code == 0x03: #collision
|
||||
col_mesh_offset = rom.read_int24(headcur + 5)
|
||||
col_mesh = CollisionMesh(rom, start, col_mesh_offset)
|
||||
self.patch_mesh(rom, col_mesh);
|
||||
|
||||
elif code == 0x04: #rooms
|
||||
room_list_offset = rom.read_int24(headcur + 5)
|
||||
|
||||
elif code == 0x0D: #paths
|
||||
path_offset = self.append_path_data(rom)
|
||||
rom.write_int32(headcur + 4, path_offset)
|
||||
|
||||
elif code == 0x0E: #transition actors
|
||||
t_offset = rom.read_int24(headcur + 5)
|
||||
addr = self.file.start + t_offset
|
||||
write_actor_data(rom, addr, self.transition_actors)
|
||||
|
||||
headcur += 8
|
||||
code = rom.read_byte(headcur)
|
||||
|
||||
# update file references
|
||||
self.file.end = align16(self.file.end)
|
||||
update_dmadata(rom, self.file)
|
||||
update_scene_table(rom, self.id, self.file.start, self.file.end)
|
||||
|
||||
# write room file data
|
||||
for room in self.rooms:
|
||||
room.write_data(rom)
|
||||
if self.id == 6 and room.id == 6:
|
||||
patch_spirit_temple_mq_room_6(rom, room.file.start)
|
||||
|
||||
cur = self.file.start + room_list_offset
|
||||
for room in self.rooms:
|
||||
rom.write_int32s(cur, [room.file.start, room.file.end])
|
||||
cur += 0x08
|
||||
|
||||
|
||||
def write_map_data(self, rom:Rom):
|
||||
if self.id >= 10:
|
||||
return
|
||||
|
||||
# write floormap
|
||||
floormap_indices = 0xB6C934
|
||||
floormap_vrom = 0xBC7E00
|
||||
floormap_index = rom.read_int16(floormap_indices + (self.id * 2))
|
||||
floormap_index //= 2 # game uses texture index, where two textures are used per floor
|
||||
|
||||
cur = floormap_vrom + (floormap_index * 0x1EC)
|
||||
for floormap in self.floormaps:
|
||||
for icon in floormap:
|
||||
Icon.write_to_floormap(icon, rom, cur)
|
||||
cur += 0xA4
|
||||
|
||||
|
||||
# fixes jabu jabu floor B1 having no chest data
|
||||
if self.id == 2:
|
||||
cur = floormap_vrom + (0x08 * 0x1EC + 4)
|
||||
kaleido_scope_chest_verts = 0x803A3DA0 # hax, should be vram 0x8082EA00
|
||||
rom.write_int32s(cur, [0x17, kaleido_scope_chest_verts, 0x04])
|
||||
|
||||
# write minimaps
|
||||
map_mark_vrom = 0xBF40D0
|
||||
map_mark_vram = 0x808567F0
|
||||
map_mark_array_vram = 0x8085D2DC # ptr array in map_mark_data to minimap "marks"
|
||||
|
||||
array_vrom = map_mark_array_vram - map_mark_vram + map_mark_vrom
|
||||
map_mark_scene_vram = rom.read_int32(self.id * 4 + array_vrom)
|
||||
mark_vrom = map_mark_scene_vram - map_mark_vram + map_mark_vrom
|
||||
|
||||
cur = mark_vrom
|
||||
for minimap in self.minimaps:
|
||||
for icon in minimap:
|
||||
Icon.write_to_minimap(icon, rom, cur)
|
||||
cur += 0x26
|
||||
|
||||
|
||||
def patch_mesh(self, rom:Rom, mesh:CollisionMesh):
|
||||
start = self.file.start
|
||||
|
||||
final_cams = []
|
||||
|
||||
# build final camera data
|
||||
for cam in self.coldelta.cams:
|
||||
data = cam['Data']
|
||||
pos = cam['PositionIndex']
|
||||
if pos < 0:
|
||||
final_cams.append((data, 0))
|
||||
else:
|
||||
addr = start + (mesh.camera_data_addr & 0xFFFFFF)
|
||||
seg_off = rom.read_int32(addr + (pos * 8) + 4)
|
||||
final_cams.append((data, seg_off))
|
||||
|
||||
types_move_addr = 0
|
||||
|
||||
# if data can't fit within the old mesh space, append camera data
|
||||
if self.coldelta.is_larger:
|
||||
types_move_addr = mesh.camera_data_addr
|
||||
|
||||
# append to end of file
|
||||
self.write_cam_data(rom, self.file.end, final_cams)
|
||||
mesh.camera_data_addr = get_segment_address(2, self.file.end - self.file.start)
|
||||
self.file.end += len(final_cams) * 8
|
||||
|
||||
else:
|
||||
types_move_addr = mesh.camera_data_addr + (len(final_cams) * 8)
|
||||
|
||||
# append in place
|
||||
addr = self.file.start + (mesh.camera_data_addr & 0xFFFFFF)
|
||||
self.write_cam_data(rom, addr, final_cams)
|
||||
|
||||
# if polytypes needs to be moved, do so
|
||||
if (types_move_addr != mesh.polytypes_addr):
|
||||
a_start = self.file.start + (mesh.polytypes_addr & 0xFFFFFF)
|
||||
b_start = self.file.start + (types_move_addr & 0xFFFFFF)
|
||||
size = mesh.polytypes * 8
|
||||
|
||||
rom.buffer[b_start:b_start + size] = rom.buffer[a_start:a_start + size]
|
||||
mesh.polytypes_addr = types_move_addr
|
||||
|
||||
# patch polytypes
|
||||
for item in self.coldelta.polytypes:
|
||||
id = item['Id']
|
||||
high = item['High']
|
||||
low = item['Low']
|
||||
addr = self.file.start + (mesh.polytypes_addr & 0xFFFFFF) + (id * 8)
|
||||
rom.write_int32s(addr, [high, low])
|
||||
|
||||
# patch poly data
|
||||
for item in self.coldelta.polys:
|
||||
id = item['Id']
|
||||
t = item['Type']
|
||||
flags = item['Flags']
|
||||
|
||||
addr = self.file.start + (mesh.poly_addr & 0xFFFFFF) + (id * 0x10)
|
||||
vert_bit = rom.read_byte(addr + 0x02) & 0x1F # VertexA id data
|
||||
rom.write_int16(addr, t)
|
||||
rom.write_byte(addr + 0x02, (flags << 5) + vert_bit)
|
||||
|
||||
# Write Mesh to Scene
|
||||
mesh.write_to_scene(rom, self.file.start)
|
||||
|
||||
|
||||
def write_cam_data(self, rom:Rom, addr, cam_data):
|
||||
|
||||
for item in cam_data:
|
||||
data, pos = item
|
||||
rom.write_int32s(addr, [data, pos])
|
||||
addr += 8
|
||||
|
||||
|
||||
# appends path data to the end of the rom
|
||||
# returns segment address to path data
|
||||
def append_path_data(self, rom:Rom):
|
||||
start = self.file.start
|
||||
cur = self.file.end
|
||||
records = []
|
||||
|
||||
for path in self.paths:
|
||||
nodes = len(path)
|
||||
offset = get_segment_address(2, cur - start)
|
||||
records.append((nodes, offset))
|
||||
|
||||
#flatten
|
||||
points = [x for points in path for x in points]
|
||||
rom.write_int16s(cur, points)
|
||||
path_size = align4(len(path) * 6)
|
||||
cur += path_size
|
||||
|
||||
records_offset = get_segment_address(2, cur - start)
|
||||
for node, offset in records:
|
||||
rom.write_byte(cur, node)
|
||||
rom.write_int32(cur + 4, offset)
|
||||
cur += 8
|
||||
|
||||
self.file.end = cur
|
||||
return records_offset
|
||||
|
||||
|
||||
class Room(object):
|
||||
def __init__(self, room):
|
||||
self.file = File(room['File'])
|
||||
self.id = room['Id']
|
||||
self.objects = [int(x, 16) for x in room['Objects']]
|
||||
self.actors = [convert_actor_data(x) for x in room['Actors']]
|
||||
|
||||
def write_data(self, rom:Rom):
|
||||
# move file to remap address
|
||||
if self.file.remap is not None:
|
||||
self.file.relocate(rom)
|
||||
|
||||
headcur = self.file.start
|
||||
|
||||
code = rom.read_byte(headcur)
|
||||
loop = 0x20
|
||||
while loop != 0 and code != 0x14: #terminator
|
||||
loop -= 1
|
||||
|
||||
if code == 0x01: # actors
|
||||
offset = self.file.end - self.file.start
|
||||
write_actor_data(rom, self.file.end, self.actors)
|
||||
self.file.end += len(self.actors) * 0x10
|
||||
|
||||
rom.write_byte(headcur + 1, len(self.actors))
|
||||
rom.write_int32(headcur + 4, get_segment_address(3, offset))
|
||||
|
||||
elif code == 0x0B: # objects
|
||||
offset = self.append_object_data(rom, self.objects)
|
||||
|
||||
rom.write_byte(headcur + 1, len(self.objects))
|
||||
rom.write_int32(headcur + 4, get_segment_address(3, offset))
|
||||
|
||||
headcur += 8
|
||||
code = rom.read_byte(headcur)
|
||||
|
||||
# update file reference
|
||||
self.file.end = align16(self.file.end)
|
||||
update_dmadata(rom, self.file)
|
||||
|
||||
|
||||
def append_object_data(self, rom:Rom, objects):
|
||||
offset = self.file.end - self.file.start
|
||||
cur = self.file.end
|
||||
rom.write_int16s(cur, objects)
|
||||
|
||||
objects_size = align4(len(objects) * 2)
|
||||
self.file.end += objects_size
|
||||
return offset
|
||||
|
||||
|
||||
def patch_files(rom:Rom, mq_scenes:list):
|
||||
|
||||
data = get_json()
|
||||
scenes = [Scene(x) for x in data]
|
||||
for scene in scenes:
|
||||
if scene.id in mq_scenes:
|
||||
if scene.id == 9:
|
||||
patch_ice_cavern_scene_header(rom)
|
||||
scene.write_data(rom)
|
||||
|
||||
|
||||
|
||||
def get_json():
|
||||
with open(data_path('mqu.json'), 'r') as stream:
|
||||
data = json.load(stream)
|
||||
return data
|
||||
|
||||
|
||||
def convert_actor_data(str):
|
||||
spawn_args = str.split(" ")
|
||||
return [ int(x,16) for x in spawn_args ]
|
||||
|
||||
|
||||
def get_segment_address(base, offset):
|
||||
offset &= 0xFFFFFF
|
||||
base *= 0x01000000
|
||||
return base + offset
|
||||
|
||||
|
||||
def patch_ice_cavern_scene_header(rom):
|
||||
rom.buffer[0x2BEB000:0x2BEB038] = rom.buffer[0x2BEB008:0x2BEB040]
|
||||
rom.write_int32s(0x2BEB038, [0x0D000000, 0x02000000])
|
||||
|
||||
|
||||
def patch_spirit_temple_mq_room_6(rom:Rom, room_addr):
|
||||
cur = room_addr
|
||||
|
||||
actor_list_addr = 0
|
||||
cmd_actors_offset = 0
|
||||
|
||||
# scan for actor list and header end
|
||||
code = rom.read_byte(cur)
|
||||
while code != 0x14: #terminator
|
||||
if code == 0x01: # actors
|
||||
actor_list_addr = rom.read_int32(cur + 4)
|
||||
cmd_actors_offset = cur - room_addr
|
||||
|
||||
cur += 8
|
||||
code = rom.read_byte(cur)
|
||||
|
||||
cur += 8
|
||||
|
||||
# original header size
|
||||
header_size = cur - room_addr
|
||||
|
||||
# set alternate header data location
|
||||
alt_data_off = header_size + 8
|
||||
|
||||
# set new alternate header offset
|
||||
alt_header_off = align16(alt_data_off + (4 * 3)) # alt header record size * num records
|
||||
|
||||
# write alternate header data
|
||||
# the first 3 words are mandatory. the last 3 are just to make the binary
|
||||
# cleaner to read
|
||||
rom.write_int32s(room_addr + alt_data_off,
|
||||
[0, get_segment_address(3, alt_header_off), 0, 0, 0, 0])
|
||||
|
||||
# clone header
|
||||
a_start = room_addr
|
||||
a_end = a_start + header_size
|
||||
b_start = room_addr + alt_header_off
|
||||
b_end = b_start + header_size
|
||||
|
||||
rom.buffer[b_start:b_end] = rom.buffer[a_start:a_end]
|
||||
|
||||
# make the child header skip the first actor,
|
||||
# which avoids the spawning of the block while in the hole
|
||||
cmd_addr = room_addr + cmd_actors_offset
|
||||
actor_list_addr += 0x10
|
||||
actors = rom.read_byte(cmd_addr + 1)
|
||||
rom.write_byte(cmd_addr+1, actors - 1)
|
||||
rom.write_int32(cmd_addr + 4, actor_list_addr)
|
||||
|
||||
# move header
|
||||
rom.buffer[a_start + 8:a_end + 8] = rom.buffer[a_start:a_end]
|
||||
|
||||
# write alternate header command
|
||||
seg = get_segment_address(3, alt_data_off)
|
||||
rom.write_int32s(room_addr, [0x18000000, seg])
|
||||
|
||||
|
||||
def verify_remap(scenes):
|
||||
def test_remap(file:File):
|
||||
if file.remap is not None:
|
||||
if file.start < file.remap:
|
||||
return False
|
||||
return True
|
||||
print("test code: verify remap won't corrupt data")
|
||||
|
||||
for scene in scenes:
|
||||
file = scene.file
|
||||
result = test_remap(file)
|
||||
print("{0} - {1}".format(result, file))
|
||||
|
||||
for room in scene.rooms:
|
||||
file = room.file
|
||||
result = test_remap(file)
|
||||
print("{0} - {1}".format(result, file))
|
||||
|
||||
|
||||
def update_dmadata(rom:Rom, file:File):
|
||||
key, start, end, from_file = file.dma_key, file.start, file.end, file.from_file
|
||||
rom.update_dmadata_record(key, start, end, from_file)
|
||||
file.dma_key = file.start
|
||||
|
||||
def update_scene_table(rom:Rom, sceneId, start, end):
|
||||
cur = sceneId * 0x14 + SCENE_TABLE
|
||||
rom.write_int32s(cur, [start, end])
|
||||
|
||||
|
||||
def write_actor_data(rom:Rom, cur, actors):
|
||||
for actor in actors:
|
||||
rom.write_int16s(cur, actor)
|
||||
cur += 0x10
|
||||
|
||||
def align4(value):
|
||||
return ((value + 3) // 4) * 4
|
||||
|
||||
def align16(value):
|
||||
return ((value + 0xF) // 0x10) * 0x10
|
||||
|
||||
# This function inserts space in a ovl section at the section's offset
|
||||
# The section size is expanded
|
||||
# Every relocation entry in the section after the offet is moved accordingly
|
||||
# Every relocation value that is after the inserted space is increased accordingly
|
||||
def insert_space(rom, file, vram_start, insert_section, insert_offset, insert_size):
|
||||
sections = []
|
||||
val_hi = {}
|
||||
adr_hi = {}
|
||||
|
||||
# get the ovl header
|
||||
cur = file.end - rom.read_int32(file.end - 4)
|
||||
section_total = 0
|
||||
for i in range(0, 4):
|
||||
# build the section offsets
|
||||
section_size = rom.read_int32(cur)
|
||||
sections.append(section_total)
|
||||
section_total += section_size
|
||||
|
||||
# increase the section to be expanded
|
||||
if insert_section == i:
|
||||
rom.write_int32(cur, section_size + insert_size)
|
||||
|
||||
cur += 4
|
||||
|
||||
# calculate the insert address in vram
|
||||
insert_vram = sections[insert_section] + insert_offset + vram_start
|
||||
insert_rom = sections[insert_section] + insert_offset + file.start
|
||||
|
||||
# iterate over the relocation table
|
||||
relocate_count = rom.read_int32(cur)
|
||||
cur += 4
|
||||
for i in range(0, relocate_count):
|
||||
entry = rom.read_int32(cur)
|
||||
|
||||
# parse relocation entry
|
||||
section = ((entry & 0xC0000000) >> 30) - 1
|
||||
type = (entry & 0x3F000000) >> 24
|
||||
offset = entry & 0x00FFFFFF
|
||||
|
||||
# calculate relocation address in rom
|
||||
address = file.start + sections[section] + offset
|
||||
|
||||
# move relocation if section is increased and it's after the insert
|
||||
if insert_section == section and offset >= insert_offset:
|
||||
# rebuild new relocation entry
|
||||
rom.write_int32(cur,
|
||||
((section + 1) << 30) |
|
||||
(type << 24) |
|
||||
(offset + insert_size))
|
||||
|
||||
# value contains the vram address
|
||||
value = rom.read_int32(address)
|
||||
raw_value = value
|
||||
if type == 2:
|
||||
# Data entry: value is the raw vram address
|
||||
pass
|
||||
elif type == 4:
|
||||
# Jump OP: Get the address from a Jump instruction
|
||||
value = 0x80000000 | (value & 0x03FFFFFF) << 2
|
||||
elif type == 5:
|
||||
# Load High: Upper half of an address load
|
||||
reg = (value >> 16) & 0x1F
|
||||
val_hi[reg] = (value & 0x0000FFFF) << 16
|
||||
adr_hi[reg] = address
|
||||
# Do not process, wait until the lower half is read
|
||||
value = None
|
||||
elif type == 6:
|
||||
# Load Low: Lower half of the address load
|
||||
reg = (value >> 21) & 0x1F
|
||||
val_low = value & 0x0000FFFF
|
||||
val_low = unpack('h', pack('H', val_low))[0]
|
||||
# combine with previous load high
|
||||
value = val_hi[reg] + val_low
|
||||
else:
|
||||
# unknown. OoT does not use any other types
|
||||
value = None
|
||||
|
||||
# update the vram values if it's been moved
|
||||
if value != None and value >= insert_vram:
|
||||
# value = new vram address
|
||||
new_value = value + insert_size
|
||||
|
||||
if type == 2:
|
||||
# Data entry: value is the raw vram address
|
||||
rom.write_int32(address, new_value)
|
||||
elif type == 4:
|
||||
# Jump OP: Set the address in the Jump instruction
|
||||
op = rom.read_int32(address) & 0xFC000000
|
||||
new_value = (new_value & 0x0FFFFFFC) >> 2
|
||||
new_value = op | new_value
|
||||
rom.write_int32(address, new_value)
|
||||
elif type == 6:
|
||||
# Load Low: Lower half of the address load
|
||||
op = rom.read_int32(address) & 0xFFFF0000
|
||||
new_val_low = new_value & 0x0000FFFF
|
||||
rom.write_int32(address, op | new_val_low)
|
||||
|
||||
# Load High: Upper half of an address load
|
||||
op = rom.read_int32(adr_hi[reg]) & 0xFFFF0000
|
||||
new_val_hi = (new_value & 0xFFFF0000) >> 16
|
||||
if new_val_low >= 0x8000:
|
||||
# add 1 if the lower part is negative for borrow
|
||||
new_val_hi += 1
|
||||
rom.write_int32(adr_hi[reg], op | new_val_hi)
|
||||
|
||||
cur += 4
|
||||
|
||||
# Move rom bytes
|
||||
rom.buffer[(insert_rom + insert_size):(file.end + insert_size)] = rom.buffer[insert_rom:file.end]
|
||||
rom.buffer[insert_rom:(insert_rom + insert_size)] = [0] * insert_size
|
||||
file.end += insert_size
|
||||
|
||||
|
||||
def add_relocations(rom, file, addresses):
|
||||
relocations = []
|
||||
sections = []
|
||||
header_size = rom.read_int32(file.end - 4)
|
||||
header = file.end - header_size
|
||||
cur = header
|
||||
|
||||
# read section sizes and build offsets
|
||||
section_total = 0
|
||||
for i in range(0, 4):
|
||||
section_size = rom.read_int32(cur)
|
||||
sections.append(section_total)
|
||||
section_total += section_size
|
||||
cur += 4
|
||||
|
||||
# get all entries in relocation table
|
||||
relocate_count = rom.read_int32(cur)
|
||||
cur += 4
|
||||
for i in range(0, relocate_count):
|
||||
relocations.append(rom.read_int32(cur))
|
||||
cur += 4
|
||||
|
||||
# create new enties
|
||||
for address in addresses:
|
||||
if isinstance(address, tuple):
|
||||
# if type provided use it
|
||||
type, address = address
|
||||
else:
|
||||
# Otherwise, try to infer type from value
|
||||
value = rom.read_int32(address)
|
||||
op = value >> 26
|
||||
type = 2 # default: data
|
||||
if op == 0x02 or op == 0x03: # j or jal
|
||||
type = 4
|
||||
elif op == 0x0F: # lui
|
||||
type = 5
|
||||
elif op == 0x08: # addi
|
||||
type = 6
|
||||
|
||||
# Calculate section and offset
|
||||
address = address - file.start
|
||||
section = 0
|
||||
for section_start in sections:
|
||||
if address >= section_start:
|
||||
section += 1
|
||||
else:
|
||||
break
|
||||
offset = address - sections[section - 1]
|
||||
|
||||
# generate relocation entry
|
||||
relocations.append((section << 30)
|
||||
| (type << 24)
|
||||
| (offset & 0x00FFFFFF))
|
||||
|
||||
# Rebuild Relocation Table
|
||||
cur = header + 0x10
|
||||
relocations.sort(key = lambda val: val & 0xC0FFFFFF)
|
||||
rom.write_int32(cur, len(relocations))
|
||||
cur += 4
|
||||
for relocation in relocations:
|
||||
rom.write_int32(cur, relocation)
|
||||
cur += 4
|
||||
|
||||
# Add padded 0?
|
||||
rom.write_int32(cur, 0)
|
||||
cur += 4
|
||||
|
||||
# Update Header and File size
|
||||
new_header_size = (cur + 4) - header
|
||||
rom.write_int32(cur, new_header_size)
|
||||
file.end += (new_header_size - header_size)
|
||||
995
worlds/oot/Messages.py
Normal file
995
worlds/oot/Messages.py
Normal file
@@ -0,0 +1,995 @@
|
||||
# text details: https://wiki.cloudmodding.com/oot/Text_Format
|
||||
|
||||
import random
|
||||
from .TextBox import line_wrap
|
||||
|
||||
TEXT_START = 0x92D000
|
||||
ENG_TEXT_SIZE_LIMIT = 0x39000
|
||||
JPN_TEXT_SIZE_LIMIT = 0x3A150
|
||||
|
||||
JPN_TABLE_START = 0xB808AC
|
||||
ENG_TABLE_START = 0xB849EC
|
||||
CREDITS_TABLE_START = 0xB88C0C
|
||||
|
||||
JPN_TABLE_SIZE = ENG_TABLE_START - JPN_TABLE_START
|
||||
ENG_TABLE_SIZE = CREDITS_TABLE_START - ENG_TABLE_START
|
||||
|
||||
EXTENDED_TABLE_START = JPN_TABLE_START # start writing entries to the jp table instead of english for more space
|
||||
EXTENDED_TABLE_SIZE = JPN_TABLE_SIZE + ENG_TABLE_SIZE # 0x8360 bytes, 4204 entries
|
||||
|
||||
# name of type, followed by number of additional bytes to read, follwed by a function that prints the code
|
||||
CONTROL_CODES = {
|
||||
0x00: ('pad', 0, lambda _: '<pad>' ),
|
||||
0x01: ('line-break', 0, lambda _: '\n' ),
|
||||
0x02: ('end', 0, lambda _: '' ),
|
||||
0x04: ('box-break', 0, lambda _: '\n▼\n' ),
|
||||
0x05: ('color', 1, lambda d: '<color ' + "{:02x}".format(d) + '>' ),
|
||||
0x06: ('gap', 1, lambda d: '<' + str(d) + 'px gap>' ),
|
||||
0x07: ('goto', 2, lambda d: '<goto ' + "{:04x}".format(d) + '>' ),
|
||||
0x08: ('instant', 0, lambda _: '<allow instant text>' ),
|
||||
0x09: ('un-instant', 0, lambda _: '<disallow instant text>' ),
|
||||
0x0A: ('keep-open', 0, lambda _: '<keep open>' ),
|
||||
0x0B: ('event', 0, lambda _: '<event>' ),
|
||||
0x0C: ('box-break-delay', 1, lambda d: '\n▼<wait ' + str(d) + ' frames>\n' ),
|
||||
0x0E: ('fade-out', 1, lambda d: '<fade after ' + str(d) + ' frames?>' ),
|
||||
0x0F: ('name', 0, lambda _: '<name>' ),
|
||||
0x10: ('ocarina', 0, lambda _: '<ocarina>' ),
|
||||
0x12: ('sound', 2, lambda d: '<play SFX ' + "{:04x}".format(d) + '>' ),
|
||||
0x13: ('icon', 1, lambda d: '<icon ' + "{:02x}".format(d) + '>' ),
|
||||
0x14: ('speed', 1, lambda d: '<delay each character by ' + str(d) + ' frames>' ),
|
||||
0x15: ('background', 3, lambda d: '<set background to ' + "{:06x}".format(d) + '>' ),
|
||||
0x16: ('marathon', 0, lambda _: '<marathon time>' ),
|
||||
0x17: ('race', 0, lambda _: '<race time>' ),
|
||||
0x18: ('points', 0, lambda _: '<points>' ),
|
||||
0x19: ('skulltula', 0, lambda _: '<skulltula count>' ),
|
||||
0x1A: ('unskippable', 0, lambda _: '<text is unskippable>' ),
|
||||
0x1B: ('two-choice', 0, lambda _: '<start two choice>' ),
|
||||
0x1C: ('three-choice', 0, lambda _: '<start three choice>' ),
|
||||
0x1D: ('fish', 0, lambda _: '<fish weight>' ),
|
||||
0x1E: ('high-score', 1, lambda d: '<high-score ' + "{:02x}".format(d) + '>' ),
|
||||
0x1F: ('time', 0, lambda _: '<current time>' ),
|
||||
}
|
||||
|
||||
SPECIAL_CHARACTERS = {
|
||||
0x80: 'À',
|
||||
0x81: 'Á',
|
||||
0x82: 'Â',
|
||||
0x83: 'Ä',
|
||||
0x84: 'Ç',
|
||||
0x85: 'È',
|
||||
0x86: 'É',
|
||||
0x87: 'Ê',
|
||||
0x88: 'Ë',
|
||||
0x89: 'Ï',
|
||||
0x8A: 'Ô',
|
||||
0x8B: 'Ö',
|
||||
0x8C: 'Ù',
|
||||
0x8D: 'Û',
|
||||
0x8E: 'Ü',
|
||||
0x8F: 'ß',
|
||||
0x90: 'à',
|
||||
0x91: 'á',
|
||||
0x92: 'â',
|
||||
0x93: 'ä',
|
||||
0x94: 'ç',
|
||||
0x95: 'è',
|
||||
0x96: 'é',
|
||||
0x97: 'ê',
|
||||
0x98: 'ë',
|
||||
0x99: 'ï',
|
||||
0x9A: 'ô',
|
||||
0x9B: 'ö',
|
||||
0x9C: 'ù',
|
||||
0x9D: 'û',
|
||||
0x9E: 'ü',
|
||||
0x9F: '[A]',
|
||||
0xA0: '[B]',
|
||||
0xA1: '[C]',
|
||||
0xA2: '[L]',
|
||||
0xA3: '[R]',
|
||||
0xA4: '[Z]',
|
||||
0xA5: '[C Up]',
|
||||
0xA6: '[C Down]',
|
||||
0xA7: '[C Left]',
|
||||
0xA8: '[C Right]',
|
||||
0xA9: '[Triangle]',
|
||||
0xAA: '[Control Stick]',
|
||||
}
|
||||
|
||||
UTF8_TO_OOT_SPECIAL = {
|
||||
(0xc3, 0x80): 0x80,
|
||||
(0xc3, 0xae): 0x81,
|
||||
(0xc3, 0x82): 0x82,
|
||||
(0xc3, 0x84): 0x83,
|
||||
(0xc3, 0x87): 0x84,
|
||||
(0xc3, 0x88): 0x85,
|
||||
(0xc3, 0x89): 0x86,
|
||||
(0xc3, 0x8a): 0x87,
|
||||
(0xc3, 0x8b): 0x88,
|
||||
(0xc3, 0x8f): 0x89,
|
||||
(0xc3, 0x94): 0x8A,
|
||||
(0xc3, 0x96): 0x8B,
|
||||
(0xc3, 0x99): 0x8C,
|
||||
(0xc3, 0x9b): 0x8D,
|
||||
(0xc3, 0x9c): 0x8E,
|
||||
(0xc3, 0x9f): 0x8F,
|
||||
(0xc3, 0xa0): 0x90,
|
||||
(0xc3, 0xa1): 0x91,
|
||||
(0xc3, 0xa2): 0x92,
|
||||
(0xc3, 0xa4): 0x93,
|
||||
(0xc3, 0xa7): 0x94,
|
||||
(0xc3, 0xa8): 0x95,
|
||||
(0xc3, 0xa9): 0x96,
|
||||
(0xc3, 0xaa): 0x97,
|
||||
(0xc3, 0xab): 0x98,
|
||||
(0xc3, 0xaf): 0x99,
|
||||
(0xc3, 0xb4): 0x9A,
|
||||
(0xc3, 0xb6): 0x9B,
|
||||
(0xc3, 0xb9): 0x9C,
|
||||
(0xc3, 0xbb): 0x9D,
|
||||
(0xc3, 0xbc): 0x9E,
|
||||
}
|
||||
|
||||
GOSSIP_STONE_MESSAGES = list( range(0x0401, 0x04FF) ) # ids of the actual hints
|
||||
GOSSIP_STONE_MESSAGES += [0x2053, 0x2054] # shared initial stone messages
|
||||
TEMPLE_HINTS_MESSAGES = [0x7057, 0x707A] # dungeon reward hints from the temple of time pedestal
|
||||
LIGHT_ARROW_HINT = [0x70CC] # ganondorf's light arrow hint line
|
||||
GS_TOKEN_MESSAGES = [0x00B4, 0x00B5] # Get Gold Skulltula Token messages
|
||||
ERROR_MESSAGE = 0x0001
|
||||
|
||||
# messages for shorter item messages
|
||||
# ids are in the space freed up by move_shop_item_messages()
|
||||
ITEM_MESSAGES = {
|
||||
0x0001: "\x08\x06\x30\x05\x41TEXT ID ERROR!\x05\x40",
|
||||
0x9001: "\x08\x13\x2DYou borrowed a \x05\x41Pocket Egg\x05\x40!\x01A Pocket Cucco will hatch from\x01it overnight. Be sure to give it\x01back.",
|
||||
0x0002: "\x08\x13\x2FYou returned the Pocket Cucco\x01and got \x05\x41Cojiro\x05\x40 in return!\x01Unlike other Cuccos, Cojiro\x01rarely crows.",
|
||||
0x0003: "\x08\x13\x30You got an \x05\x41Odd Mushroom\x05\x40!\x01It is sure to spoil quickly! Take\x01it to the Kakariko Potion Shop.",
|
||||
0x0004: "\x08\x13\x31You received an \x05\x41Odd Potion\x05\x40!\x01It may be useful for something...\x01Hurry to the Lost Woods!",
|
||||
0x0005: "\x08\x13\x32You returned the Odd Potion \x01and got the \x05\x41Poacher's Saw\x05\x40!\x01The young punk guy must have\x01left this.",
|
||||
0x0007: "\x08\x13\x48You got a \x01\x05\x41Deku Seeds Bullet Bag\x05\x40.\x01This bag can hold up to \x05\x4640\x05\x40\x01slingshot bullets.",
|
||||
0x0008: "\x08\x13\x33You traded the Poacher's Saw \x01for a \x05\x41Broken Goron's Sword\x05\x40!\x01Visit Biggoron to get it repaired!",
|
||||
0x0009: "\x08\x13\x34You checked in the Broken \x01Goron's Sword and received a \x01\x05\x41Prescription\x05\x40!\x01Go see King Zora!",
|
||||
0x000A: "\x08\x13\x37The Biggoron's Sword...\x01You got a \x05\x41Claim Check \x05\x40for it!\x01You can't wait for the sword!",
|
||||
0x000B: "\x08\x13\x2EYou got a \x05\x41Pocket Cucco, \x05\x40one\x01of Anju's prized hens! It fits \x01in your pocket.",
|
||||
0x000C: "\x08\x13\x3DYou got the \x05\x41Biggoron's Sword\x05\x40!\x01This blade was forged by a \x01master smith and won't break!",
|
||||
0x000D: "\x08\x13\x35You used the Prescription and\x01received an \x05\x41Eyeball Frog\x05\x40!\x01Be quick and deliver it to Lake \x01Hylia!",
|
||||
0x000E: "\x08\x13\x36You traded the Eyeball Frog \x01for the \x05\x41World's Finest Eye Drops\x05\x40!\x01Hurry! Take them to Biggoron!",
|
||||
0x0010: "\x08\x13\x25You borrowed a \x05\x41Skull Mask\x05\x40.\x01You feel like a monster while you\x01wear this mask!",
|
||||
0x0011: "\x08\x13\x26You borrowed a \x05\x41Spooky Mask\x05\x40.\x01You can scare many people\x01with this mask!",
|
||||
0x0012: "\x08\x13\x24You borrowed a \x05\x41Keaton Mask\x05\x40.\x01You'll be a popular guy with\x01this mask on!",
|
||||
0x0013: "\x08\x13\x27You borrowed a \x05\x41Bunny Hood\x05\x40.\x01The hood's long ears are so\x01cute!",
|
||||
0x0014: "\x08\x13\x28You borrowed a \x05\x41Goron Mask\x05\x40.\x01It will make your head look\x01big, though.",
|
||||
0x0015: "\x08\x13\x29You borrowed a \x05\x41Zora Mask\x05\x40.\x01With this mask, you can\x01become one of the Zoras!",
|
||||
0x0016: "\x08\x13\x2AYou borrowed a \x05\x41Gerudo Mask\x05\x40.\x01This mask will make you look\x01like...a girl?",
|
||||
0x0017: "\x08\x13\x2BYou borrowed a \x05\x41Mask of Truth\x05\x40.\x01Show it to many people!",
|
||||
0x0030: "\x08\x13\x06You found the \x05\x41Fairy Slingshot\x05\x40!",
|
||||
0x0031: "\x08\x13\x03You found the \x05\x41Fairy Bow\x05\x40!",
|
||||
0x0032: "\x08\x13\x02You got \x05\x41Bombs\x05\x40!\x01If you see something\x01suspicious, bomb it!",
|
||||
0x0033: "\x08\x13\x09You got \x05\x41Bombchus\x05\x40!",
|
||||
0x0034: "\x08\x13\x01You got a \x05\x41Deku Nut\x05\x40!",
|
||||
0x0035: "\x08\x13\x0EYou found the \x05\x41Boomerang\x05\x40!",
|
||||
0x0036: "\x08\x13\x0AYou found the \x05\x41Hookshot\x05\x40!\x01It's a spring-loaded chain that\x01you can cast out to hook things.",
|
||||
0x0037: "\x08\x13\x00You got a \x05\x41Deku Stick\x05\x40!",
|
||||
0x0038: "\x08\x13\x11You found the \x05\x41Megaton Hammer\x05\x40!\x01It's so heavy, you need to\x01use two hands to swing it!",
|
||||
0x0039: "\x08\x13\x0FYou found the \x05\x41Lens of Truth\x05\x40!\x01Mysterious things are hidden\x01everywhere!",
|
||||
0x003A: "\x08\x13\x08You found the \x05\x41Ocarina of Time\x05\x40!\x01It glows with a mystical light...",
|
||||
0x003C: "\x08\x13\x67You received the \x05\x41Fire\x01Medallion\x05\x40!\x01Darunia awakens as a Sage and\x01adds his power to yours!",
|
||||
0x003D: "\x08\x13\x68You received the \x05\x43Water\x01Medallion\x05\x40!\x01Ruto awakens as a Sage and\x01adds her power to yours!",
|
||||
0x003E: "\x08\x13\x66You received the \x05\x42Forest\x01Medallion\x05\x40!\x01Saria awakens as a Sage and\x01adds her power to yours!",
|
||||
0x003F: "\x08\x13\x69You received the \x05\x46Spirit\x01Medallion\x05\x40!\x01Nabooru awakens as a Sage and\x01adds her power to yours!",
|
||||
0x0040: "\x08\x13\x6BYou received the \x05\x44Light\x01Medallion\x05\x40!\x01Rauru the Sage adds his power\x01to yours!",
|
||||
0x0041: "\x08\x13\x6AYou received the \x05\x45Shadow\x01Medallion\x05\x40!\x01Impa awakens as a Sage and\x01adds her power to yours!",
|
||||
0x0042: "\x08\x13\x14You got an \x05\x41Empty Bottle\x05\x40!\x01You can put something in this\x01bottle.",
|
||||
0x0043: "\x08\x13\x15You got a \x05\x41Red Potion\x05\x40!\x01It will restore your health",
|
||||
0x0044: "\x08\x13\x16You got a \x05\x42Green Potion\x05\x40!\x01It will restore your magic.",
|
||||
0x0045: "\x08\x13\x17You got a \x05\x43Blue Potion\x05\x40!\x01It will recover your health\x01and magic.",
|
||||
0x0046: "\x08\x13\x18You caught a \x05\x41Fairy\x05\x40 in a bottle!\x01It will revive you\x01the moment you run out of life \x01energy.",
|
||||
0x0047: "\x08\x13\x19You got a \x05\x41Fish\x05\x40!\x01It looks so fresh and\x01delicious!",
|
||||
0x0048: "\x08\x13\x10You got a \x05\x41Magic Bean\x05\x40!\x01Find a suitable spot for a garden\x01and plant it.",
|
||||
0x9048: "\x08\x13\x10You got a \x05\x41Pack of Magic Beans\x05\x40!\x01Find suitable spots for a garden\x01and plant them.",
|
||||
0x004A: "\x08\x13\x07You received the \x05\x41Fairy Ocarina\x05\x40!\x01This is a memento from Saria.",
|
||||
0x004B: "\x08\x13\x3DYou got the \x05\x42Giant's Knife\x05\x40!\x01Hold it with both hands to\x01attack! It's so long, you\x01can't use it with a \x05\x44shield\x05\x40.",
|
||||
0x004C: "\x08\x13\x3EYou got a \x05\x44Deku Shield\x05\x40!",
|
||||
0x004D: "\x08\x13\x3FYou got a \x05\x44Hylian Shield\x05\x40!",
|
||||
0x004E: "\x08\x13\x40You found the \x05\x44Mirror Shield\x05\x40!\x01The shield's polished surface can\x01reflect light or energy.",
|
||||
0x004F: "\x08\x13\x0BYou found the \x05\x41Longshot\x05\x40!\x01It's an upgraded Hookshot.\x01It extends \x05\x41twice\x05\x40 as far!",
|
||||
0x0050: "\x08\x13\x42You got a \x05\x41Goron Tunic\x05\x40!\x01Going to a hot place? No worry!",
|
||||
0x0051: "\x08\x13\x43You got a \x05\x43Zora Tunic\x05\x40!\x01Wear it, and you won't drown\x01underwater.",
|
||||
0x0052: "\x08You got a \x05\x42Magic Jar\x05\x40!\x01Your Magic Meter is filled!",
|
||||
0x0053: "\x08\x13\x45You got the \x05\x41Iron Boots\x05\x40!\x01So heavy, you can't run.\x01So heavy, you can't float.",
|
||||
0x0054: "\x08\x13\x46You got the \x05\x41Hover Boots\x05\x40!\x01With these mysterious boots\x01you can hover above the ground.",
|
||||
0x0055: "\x08You got a \x05\x45Recovery Heart\x05\x40!\x01Your life energy is recovered!",
|
||||
0x0056: "\x08\x13\x4BYou upgraded your quiver to a\x01\x05\x41Big Quiver\x05\x40!\x01Now you can carry more arrows-\x01\x05\x4640 \x05\x40in total!",
|
||||
0x0057: "\x08\x13\x4CYou upgraded your quiver to\x01the \x05\x41Biggest Quiver\x05\x40!\x01Now you can carry to a\x01maximum of \x05\x4650\x05\x40 arrows!",
|
||||
0x0058: "\x08\x13\x4DYou found a \x05\x41Bomb Bag\x05\x40!\x01You found \x05\x4120 Bombs\x05\x40 inside!",
|
||||
0x0059: "\x08\x13\x4EYou got a \x05\x41Big Bomb Bag\x05\x40!\x01Now you can carry more \x01Bombs, up to a maximum of \x05\x4630\x05\x40!",
|
||||
0x005A: "\x08\x13\x4FYou got the \x01\x05\x41Biggest Bomb Bag\x05\x40!\x01Now, you can carry up to \x01\x05\x4640\x05\x40 Bombs!",
|
||||
0x005B: "\x08\x13\x51You found the \x05\x43Silver Gauntlets\x05\x40!\x01You feel the power to lift\x01big things with it!",
|
||||
0x005C: "\x08\x13\x52You found the \x05\x43Golden Gauntlets\x05\x40!\x01You can feel even more power\x01coursing through your arms!",
|
||||
0x005D: "\x08\x13\x1CYou put a \x05\x44Blue Fire\x05\x40\x01into the bottle!\x01This is a cool flame you can\x01use on red ice.",
|
||||
0x005E: "\x08\x13\x56You got an \x05\x43Adult's Wallet\x05\x40!\x01Now you can hold\x01up to \x05\x46200\x05\x40 \x05\x46Rupees\x05\x40.",
|
||||
0x005F: "\x08\x13\x57You got a \x05\x43Giant's Wallet\x05\x40!\x01Now you can hold\x01up to \x05\x46500\x05\x40 \x05\x46Rupees\x05\x40.",
|
||||
0x0060: "\x08\x13\x77You found a \x05\x41Small Key\x05\x40!\x01This key will open a locked \x01door. You can use it only\x01in this dungeon.",
|
||||
0x0066: "\x08\x13\x76You found the \x05\x41Dungeon Map\x05\x40!\x01It's the map to this dungeon.",
|
||||
0x0067: "\x08\x13\x75You found the \x05\x41Compass\x05\x40!\x01Now you can see the locations\x01of many hidden things in the\x01dungeon!",
|
||||
0x0068: "\x08\x13\x6FYou obtained the \x05\x41Stone of Agony\x05\x40!\x01If you equip a \x05\x44Rumble Pak\x05\x40, it\x01will react to nearby...secrets.",
|
||||
0x0069: "\x08\x13\x23You received \x05\x41Zelda's Letter\x05\x40!\x01Wow! This letter has Princess\x01Zelda's autograph!",
|
||||
0x006C: "\x08\x13\x49Your \x05\x41Deku Seeds Bullet Bag \x01\x05\x40has become bigger!\x01This bag can hold \x05\x4650\x05\x41 \x05\x40bullets!",
|
||||
0x006F: "\x08You got a \x05\x42Green Rupee\x05\x40!\x01That's \x05\x42one Rupee\x05\x40!",
|
||||
0x0070: "\x08\x13\x04You got the \x05\x41Fire Arrow\x05\x40!\x01If you hit your target,\x01it will catch fire.",
|
||||
0x0071: "\x08\x13\x0CYou got the \x05\x43Ice Arrow\x05\x40!\x01If you hit your target,\x01it will freeze.",
|
||||
0x0072: "\x08\x13\x12You got the \x05\x44Light Arrow\x05\x40!\x01The light of justice\x01will smite evil!",
|
||||
0x0073: "\x08\x06\x28You have learned the\x01\x06\x2F\x05\x42Minuet of Forest\x05\x40!",
|
||||
0x0074: "\x08\x06\x28You have learned the\x01\x06\x37\x05\x41Bolero of Fire\x05\x40!",
|
||||
0x0075: "\x08\x06\x28You have learned the\x01\x06\x29\x05\x43Serenade of Water\x05\x40!",
|
||||
0x0076: "\x08\x06\x28You have learned the\x01\x06\x2D\x05\x46Requiem of Spirit\x05\x40!",
|
||||
0x0077: "\x08\x06\x28You have learned the\x01\x06\x28\x05\x45Nocturne of Shadow\x05\x40!",
|
||||
0x0078: "\x08\x06\x28You have learned the\x01\x06\x32\x05\x44Prelude of Light\x05\x40!",
|
||||
0x0079: "\x08\x13\x50You got the \x05\x41Goron's Bracelet\x05\x40!\x01Now you can pull up Bomb\x01Flowers.",
|
||||
0x007A: "\x08\x13\x1DYou put a \x05\x41Bug \x05\x40in the bottle!\x01This kind of bug prefers to\x01live in small holes in the ground.",
|
||||
0x007B: "\x08\x13\x70You obtained the \x05\x41Gerudo's \x01Membership Card\x05\x40!\x01You can get into the Gerudo's\x01training ground.",
|
||||
0x0080: "\x08\x13\x6CYou got the \x05\x42Kokiri's Emerald\x05\x40!\x01This is the Spiritual Stone of \x01Forest passed down by the\x01Great Deku Tree.",
|
||||
0x0081: "\x08\x13\x6DYou obtained the \x05\x41Goron's Ruby\x05\x40!\x01This is the Spiritual Stone of \x01Fire passed down by the Gorons!",
|
||||
0x0082: "\x08\x13\x6EYou obtained \x05\x43Zora's Sapphire\x05\x40!\x01This is the Spiritual Stone of\x01Water passed down by the\x01Zoras!",
|
||||
0x0090: "\x08\x13\x00Now you can pick up \x01many \x05\x41Deku Sticks\x05\x40!\x01You can carry up to \x05\x4620\x05\x40 of them!",
|
||||
0x0091: "\x08\x13\x00You can now pick up \x01even more \x05\x41Deku Sticks\x05\x40!\x01You can carry up to \x05\x4630\x05\x40 of them!",
|
||||
0x0097: "\x08\x13\x20You caught a \x05\x41Poe \x05\x40in a bottle!\x01Something good might happen!",
|
||||
0x0098: "\x08\x13\x1AYou got \x05\x41Lon Lon Milk\x05\x40!\x01This milk is very nutritious!\x01There are two drinks in it.",
|
||||
0x0099: "\x08\x13\x1BYou found \x05\x41Ruto's Letter\x05\x40 in a\x01bottle! Show it to King Zora.",
|
||||
0x9099: "\x08\x13\x1BYou found \x05\x41a letter in a bottle\x05\x40!\x01You remove the letter from the\x01bottle, freeing it for other uses.",
|
||||
0x009A: "\x08\x13\x21You got a \x05\x41Weird Egg\x05\x40!\x01Feels like there's something\x01moving inside!",
|
||||
0x00A4: "\x08\x13\x3BYou got the \x05\x42Kokiri Sword\x05\x40!\x01This is a hidden treasure of\x01the Kokiri.",
|
||||
0x00A7: "\x08\x13\x01Now you can carry\x01many \x05\x41Deku Nuts\x05\x40!\x01You can hold up to \x05\x4630\x05\x40 nuts!",
|
||||
0x00A8: "\x08\x13\x01You can now carry even\x01more \x05\x41Deku Nuts\x05\x40! You can carry\x01up to \x05\x4640\x05\x41 \x05\x40nuts!",
|
||||
0x00AD: "\x08\x13\x05You got \x05\x41Din's Fire\x05\x40!\x01Its fireball engulfs everything!",
|
||||
0x00AE: "\x08\x13\x0DYou got \x05\x42Farore's Wind\x05\x40!\x01This is warp magic you can use!",
|
||||
0x00AF: "\x08\x13\x13You got \x05\x43Nayru's Love\x05\x40!\x01Cast this to create a powerful\x01protective barrier.",
|
||||
0x00B4: "\x08You got a \x05\x41Gold Skulltula Token\x05\x40!\x01You've collected \x05\x41\x19\x05\x40 tokens in total.",
|
||||
0x00B5: "\x08You destroyed a \x05\x41Gold Skulltula\x05\x40.\x01You got a token proving you \x01destroyed it!", #Unused
|
||||
0x00C2: "\x08\x13\x73You got a \x05\x41Piece of Heart\x05\x40!\x01Collect four pieces total to get\x01another Heart Container.",
|
||||
0x00C3: "\x08\x13\x73You got a \x05\x41Piece of Heart\x05\x40!\x01So far, you've collected two \x01pieces.",
|
||||
0x00C4: "\x08\x13\x73You got a \x05\x41Piece of Heart\x05\x40!\x01Now you've collected three \x01pieces!",
|
||||
0x00C5: "\x08\x13\x73You got a \x05\x41Piece of Heart\x05\x40!\x01You've completed another Heart\x01Container!",
|
||||
0x00C6: "\x08\x13\x72You got a \x05\x41Heart Container\x05\x40!\x01Your maximum life energy is \x01increased by one heart.",
|
||||
0x00C7: "\x08\x13\x74You got the \x05\x41Boss Key\x05\x40!\x01Now you can get inside the \x01chamber where the Boss lurks.",
|
||||
0x9002: "\x08You are a \x05\x43FOOL\x05\x40!",
|
||||
0x00CC: "\x08You got a \x05\x43Blue Rupee\x05\x40!\x01That's \x05\x43five Rupees\x05\x40!",
|
||||
0x00CD: "\x08\x13\x53You got the \x05\x43Silver Scale\x05\x40!\x01You can dive deeper than you\x01could before.",
|
||||
0x00CE: "\x08\x13\x54You got the \x05\x43Golden Scale\x05\x40!\x01Now you can dive much\x01deeper than you could before!",
|
||||
0x00D1: "\x08\x06\x14You've learned \x05\x42Saria's Song\x05\x40!",
|
||||
0x00D2: "\x08\x06\x11You've learned \x05\x41Epona's Song\x05\x40!",
|
||||
0x00D3: "\x08\x06\x0BYou've learned the \x05\x46Sun's Song\x05\x40!",
|
||||
0x00D4: "\x08\x06\x15You've learned \x05\x43Zelda's Lullaby\x05\x40!",
|
||||
0x00D5: "\x08\x06\x05You've learned the \x05\x44Song of Time\x05\x40!",
|
||||
0x00D6: "\x08You've learned the \x05\x45Song of Storms\x05\x40!",
|
||||
0x00DC: "\x08\x13\x58You got \x05\x41Deku Seeds\x05\x40!\x01Use these as bullets\x01for your Slingshot.",
|
||||
0x00DD: "\x08You mastered the secret sword\x01technique of the \x05\x41Spin Attack\x05\x40!",
|
||||
0x00E4: "\x08You can now use \x05\x42Magic\x05\x40!",
|
||||
0x00E5: "\x08Your \x05\x44defensive power\x05\x40 is enhanced!",
|
||||
0x00E6: "\x08You got a \x05\x46bundle of arrows\x05\x40!",
|
||||
0x00E8: "\x08Your magic power has been \x01enhanced! Now you have twice\x01as much \x05\x41Magic Power\x05\x40!",
|
||||
0x00E9: "\x08Your defensive power has been \x01enhanced! Damage inflicted by \x01enemies will be \x05\x41reduced by half\x05\x40.",
|
||||
0x00F0: "\x08You got a \x05\x41Red Rupee\x05\x40!\x01That's \x05\x41twenty Rupees\x05\x40!",
|
||||
0x00F1: "\x08You got a \x05\x45Purple Rupee\x05\x40!\x01That's \x05\x45fifty Rupees\x05\x40!",
|
||||
0x00F2: "\x08You got a \x05\x46Huge Rupee\x05\x40!\x01This Rupee is worth a whopping\x01\x05\x46two hundred Rupees\x05\x40!",
|
||||
0x00F9: "\x08\x13\x1EYou put a \x05\x41Big Poe \x05\x40in a bottle!\x01Let's sell it at the \x05\x41Ghost Shop\x05\x40!\x01Something good might happen!",
|
||||
0x9003: "\x08You found a piece of the \x05\x41Triforce\x05\x40!",
|
||||
}
|
||||
|
||||
KEYSANITY_MESSAGES = {
|
||||
0x001C: "\x13\x74\x08You got the \x05\x41Boss Key\x05\x40\x01for the \x05\x41Fire Temple\x05\x40!\x09",
|
||||
0x0006: "\x13\x74\x08You got the \x05\x41Boss Key\x05\x40\x01for the \x05\x42Forest Temple\x05\x40!\x09",
|
||||
0x001D: "\x13\x74\x08You got the \x05\x41Boss Key\x05\x40\x01for the \x05\x43Water Temple\x05\x40!\x09",
|
||||
0x001E: "\x13\x74\x08You got the \x05\x41Boss Key\x05\x40\x01for the \x05\x46Spirit Temple\x05\x40!\x09",
|
||||
0x002A: "\x13\x74\x08You got the \x05\x41Boss Key\x05\x40\x01for the \x05\x45Shadow Temple\x05\x40!\x09",
|
||||
0x0061: "\x13\x74\x08You got the \x05\x41Boss Key\x05\x40\x01for \x05\x41Ganon's Castle\x05\x40!\x09",
|
||||
0x0062: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for the \x05\x42Deku Tree\x05\x40!\x09",
|
||||
0x0063: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for \x05\x41Dodongo's Cavern\x05\x40!\x09",
|
||||
0x0064: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for \x05\x43Jabu Jabu's Belly\x05\x40!\x09",
|
||||
0x0065: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for the \x05\x42Forest Temple\x05\x40!\x09",
|
||||
0x007C: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for the \x05\x41Fire Temple\x05\x40!\x09",
|
||||
0x007D: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for the \x05\x43Water Temple\x05\x40!\x09",
|
||||
0x007E: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for the \x05\x46Spirit Temple\x05\x40!\x09",
|
||||
0x007F: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for the \x05\x45Shadow Temple\x05\x40!\x09",
|
||||
0x0087: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for the \x05\x44Ice Cavern\x05\x40!\x09",
|
||||
0x0088: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for the \x05\x42Deku Tree\x05\x40!\x09",
|
||||
0x0089: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for \x05\x41Dodongo's Cavern\x05\x40!\x09",
|
||||
0x008A: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for \x05\x43Jabu Jabu's Belly\x05\x40!\x09",
|
||||
0x008B: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for the \x05\x42Forest Temple\x05\x40!\x09",
|
||||
0x008C: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for the \x05\x41Fire Temple\x05\x40!\x09",
|
||||
0x008E: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for the \x05\x43Water Temple\x05\x40!\x09",
|
||||
0x008F: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for the \x05\x46Spirit Temple\x05\x40!\x09",
|
||||
0x0092: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for the \x05\x44Ice Cavern\x05\x40!\x09",
|
||||
0x0093: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x42Forest Temple\x05\x40!\x09",
|
||||
0x0094: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x41Fire Temple\x05\x40!\x09",
|
||||
0x0095: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x43Water Temple\x05\x40!\x09",
|
||||
0x009B: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x45Bottom of the Well\x05\x40!\x09",
|
||||
0x009F: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x46Gerudo Training\x01Grounds\x05\x40!\x09",
|
||||
0x00A0: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x46Gerudo's Fortress\x05\x40!\x09",
|
||||
0x00A1: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for \x05\x41Ganon's Castle\x05\x40!\x09",
|
||||
0x00A2: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for the \x05\x45Bottom of the Well\x05\x40!\x09",
|
||||
0x00A3: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for the \x05\x45Shadow Temple\x05\x40!\x09",
|
||||
0x00A5: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for the \x05\x45Bottom of the Well\x05\x40!\x09",
|
||||
0x00A6: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x46Spirit Temple\x05\x40!\x09",
|
||||
0x00A9: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x45Shadow Temple\x05\x40!\x09",
|
||||
}
|
||||
|
||||
MISC_MESSAGES = {
|
||||
0x507B: (bytearray(
|
||||
b"\x08I tell you, I saw him!\x04" \
|
||||
b"\x08I saw the ghostly figure of Damp\x96\x01" \
|
||||
b"the gravekeeper sinking into\x01" \
|
||||
b"his grave. It looked like he was\x01" \
|
||||
b"holding some kind of \x05\x41treasure\x05\x40!\x02"
|
||||
), None),
|
||||
0x0422: ("They say that once \x05\x41Morpha's Curse\x05\x40\x01is lifted, striking \x05\x42this stone\x05\x40 can\x01shift the tides of \x05\x44Lake Hylia\x05\x40.\x02", 0x23),
|
||||
0x401C: ("Please find my dear \05\x41Princess Ruto\x05\x40\x01immediately... Zora!\x12\x68\x7A", 0x23),
|
||||
0x9100: ("I am out of goods now.\x01Sorry!\x04The mark that will lead you to\x01the Spirit Temple is the \x05\x41flag on\x01the left \x05\x40outside the shop.\x01Be seeing you!\x02", 0x00)
|
||||
}
|
||||
|
||||
|
||||
# convert byte array to an integer
|
||||
def bytes_to_int(bytes, signed=False):
|
||||
return int.from_bytes(bytes, byteorder='big', signed=signed)
|
||||
|
||||
|
||||
# convert int to an array of bytes of the given width
|
||||
def int_to_bytes(num, width, signed=False):
|
||||
return int.to_bytes(num, width, byteorder='big', signed=signed)
|
||||
|
||||
|
||||
def display_code_list(codes):
|
||||
message = ""
|
||||
for code in codes:
|
||||
message += str(code)
|
||||
return message
|
||||
|
||||
|
||||
def parse_control_codes(text):
|
||||
if isinstance(text, list):
|
||||
bytes = text
|
||||
elif isinstance(text, bytearray):
|
||||
bytes = list(text)
|
||||
else:
|
||||
bytes = list(text.encode('utf-8'))
|
||||
|
||||
# Special characters encoded to utf-8 must be re-encoded to OoT's values for them.
|
||||
# Tuple is used due to utf-8 encoding using two bytes.
|
||||
i = 0
|
||||
while i < len(bytes) - 1:
|
||||
if (bytes[i], bytes[i+1]) in UTF8_TO_OOT_SPECIAL:
|
||||
bytes[i] = UTF8_TO_OOT_SPECIAL[(bytes[i], bytes[i+1])]
|
||||
del bytes[i+1]
|
||||
i += 1
|
||||
|
||||
text_codes = []
|
||||
index = 0
|
||||
while index < len(bytes):
|
||||
next_char = bytes[index]
|
||||
data = 0
|
||||
index += 1
|
||||
if next_char in CONTROL_CODES:
|
||||
extra_bytes = CONTROL_CODES[next_char][1]
|
||||
if extra_bytes > 0:
|
||||
data = bytes_to_int(bytes[index : index + extra_bytes])
|
||||
index += extra_bytes
|
||||
text_code = Text_Code(next_char, data)
|
||||
text_codes.append(text_code)
|
||||
if text_code.code == 0x02: # message end code
|
||||
break
|
||||
|
||||
return text_codes
|
||||
|
||||
|
||||
# holds a single character or control code of a string
|
||||
class Text_Code():
|
||||
|
||||
def display(self):
|
||||
if self.code in CONTROL_CODES:
|
||||
return CONTROL_CODES[self.code][2](self.data)
|
||||
elif self.code in SPECIAL_CHARACTERS:
|
||||
return SPECIAL_CHARACTERS[self.code]
|
||||
elif self.code >= 0x7F:
|
||||
return '?'
|
||||
else:
|
||||
return chr(self.code)
|
||||
|
||||
def get_python_string(self):
|
||||
if self.code in CONTROL_CODES:
|
||||
ret = ''
|
||||
subdata = self.data
|
||||
for _ in range(0, CONTROL_CODES[self.code][1]):
|
||||
ret = ('\\x%02X' % (subdata & 0xFF)) + ret
|
||||
subdata = subdata >> 8
|
||||
ret = '\\x%02X' % self.code + ret
|
||||
return ret
|
||||
elif self.code in SPECIAL_CHARACTERS:
|
||||
return '\\x%02X' % self.code
|
||||
elif self.code >= 0x7F:
|
||||
return '?'
|
||||
else:
|
||||
return chr(self.code)
|
||||
|
||||
def get_string(self):
|
||||
if self.code in CONTROL_CODES:
|
||||
ret = ''
|
||||
subdata = self.data
|
||||
for _ in range(0, CONTROL_CODES[self.code][1]):
|
||||
ret = chr(subdata & 0xFF) + ret
|
||||
subdata = subdata >> 8
|
||||
ret = chr(self.code) + ret
|
||||
return ret
|
||||
else:
|
||||
return chr(self.code)
|
||||
|
||||
# writes the code to the given offset, and returns the offset of the next byte
|
||||
def size(self):
|
||||
size = 1
|
||||
if self.code in CONTROL_CODES:
|
||||
size += CONTROL_CODES[self.code][1]
|
||||
return size
|
||||
|
||||
# writes the code to the given offset, and returns the offset of the next byte
|
||||
def write(self, rom, offset):
|
||||
rom.write_byte(TEXT_START + offset, self.code)
|
||||
|
||||
extra_bytes = 0
|
||||
if self.code in CONTROL_CODES:
|
||||
extra_bytes = CONTROL_CODES[self.code][1]
|
||||
bytes_to_write = int_to_bytes(self.data, extra_bytes)
|
||||
rom.write_bytes(TEXT_START + offset + 1, bytes_to_write)
|
||||
|
||||
return offset + 1 + extra_bytes
|
||||
|
||||
def __init__(self, code, data):
|
||||
self.code = code
|
||||
if code in CONTROL_CODES:
|
||||
self.type = CONTROL_CODES[code][0]
|
||||
else:
|
||||
self.type = 'character'
|
||||
self.data = data
|
||||
|
||||
__str__ = __repr__ = display
|
||||
|
||||
# holds a single message, and all its data
|
||||
class Message():
|
||||
|
||||
def display(self):
|
||||
meta_data = ["#" + str(self.index),
|
||||
"ID: 0x" + "{:04x}".format(self.id),
|
||||
"Offset: 0x" + "{:06x}".format(self.offset),
|
||||
"Length: 0x" + "{:04x}".format(self.unpadded_length) + "/0x" + "{:04x}".format(self.length),
|
||||
"Box Type: " + str(self.box_type),
|
||||
"Postion: " + str(self.position)]
|
||||
return ', '.join(meta_data) + '\n' + self.text
|
||||
|
||||
def get_python_string(self):
|
||||
ret = ''
|
||||
for code in self.text_codes:
|
||||
ret = ret + code.get_python_string()
|
||||
return ret
|
||||
|
||||
# check if this is an unused message that just contains it's own id as text
|
||||
def is_id_message(self):
|
||||
if self.unpadded_length == 5:
|
||||
for i in range(4):
|
||||
code = self.text_codes[i].code
|
||||
if not (code in range(ord('0'),ord('9')+1) or code in range(ord('A'),ord('F')+1) or code in range(ord('a'),ord('f')+1) ):
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def parse_text(self):
|
||||
self.text_codes = parse_control_codes(self.raw_text)
|
||||
|
||||
index = 0
|
||||
for text_code in self.text_codes:
|
||||
index += text_code.size()
|
||||
if text_code.code == 0x02: # message end code
|
||||
break
|
||||
if text_code.code == 0x07: # goto
|
||||
self.has_goto = True
|
||||
self.ending = text_code
|
||||
if text_code.code == 0x0A: # keep-open
|
||||
self.has_keep_open = True
|
||||
self.ending = text_code
|
||||
if text_code.code == 0x0B: # event
|
||||
self.has_event = True
|
||||
self.ending = text_code
|
||||
if text_code.code == 0x0E: # fade out
|
||||
self.has_fade = True
|
||||
self.ending = text_code
|
||||
if text_code.code == 0x10: # ocarina
|
||||
self.has_ocarina = True
|
||||
self.ending = text_code
|
||||
if text_code.code == 0x1B: # two choice
|
||||
self.has_two_choice = True
|
||||
if text_code.code == 0x1C: # three choice
|
||||
self.has_three_choice = True
|
||||
self.text = display_code_list(self.text_codes)
|
||||
self.unpadded_length = index
|
||||
|
||||
def is_basic(self):
|
||||
return not (self.has_goto or self.has_keep_open or self.has_event or self.has_fade or self.has_ocarina or self.has_two_choice or self.has_three_choice)
|
||||
|
||||
|
||||
# computes the size of a message, including padding
|
||||
def size(self):
|
||||
size = 0
|
||||
|
||||
for code in self.text_codes:
|
||||
size += code.size()
|
||||
|
||||
size = (size + 3) & -4 # align to nearest 4 bytes
|
||||
|
||||
return size
|
||||
|
||||
# applies whatever transformations we want to the dialogs
|
||||
def transform(self, replace_ending=False, ending=None, always_allow_skip=True, speed_up_text=True):
|
||||
|
||||
ending_codes = [0x02, 0x07, 0x0A, 0x0B, 0x0E, 0x10]
|
||||
box_breaks = [0x04, 0x0C]
|
||||
slows_text = [0x08, 0x09, 0x14]
|
||||
|
||||
text_codes = []
|
||||
|
||||
# # speed the text
|
||||
if speed_up_text:
|
||||
text_codes.append(Text_Code(0x08, 0)) # allow instant
|
||||
|
||||
# write the message
|
||||
for code in self.text_codes:
|
||||
# ignore ending codes if it's going to be replaced
|
||||
if replace_ending and code.code in ending_codes:
|
||||
pass
|
||||
# ignore the "make unskippable flag"
|
||||
elif always_allow_skip and code.code == 0x1A:
|
||||
pass
|
||||
# ignore anything that slows down text
|
||||
elif speed_up_text and code.code in slows_text:
|
||||
pass
|
||||
elif speed_up_text and code.code in box_breaks:
|
||||
# some special cases for text that needs to be on a timer
|
||||
if (self.id == 0x605A or # twinrova transformation
|
||||
self.id == 0x706C or # raru ending text
|
||||
self.id == 0x70DD or # ganondorf ending text
|
||||
self.id == 0x7070): # zelda ending text
|
||||
text_codes.append(code)
|
||||
text_codes.append(Text_Code(0x08, 0)) # allow instant
|
||||
else:
|
||||
text_codes.append(Text_Code(0x04, 0)) # un-delayed break
|
||||
text_codes.append(Text_Code(0x08, 0)) # allow instant
|
||||
else:
|
||||
text_codes.append(code)
|
||||
|
||||
if replace_ending:
|
||||
if ending:
|
||||
if speed_up_text and ending.code == 0x10: # ocarina
|
||||
text_codes.append(Text_Code(0x09, 0)) # disallow instant text
|
||||
text_codes.append(ending) # write special ending
|
||||
text_codes.append(Text_Code(0x02, 0)) # write end code
|
||||
|
||||
self.text_codes = text_codes
|
||||
|
||||
|
||||
# writes a Message back into the rom, using the given index and offset to update the table
|
||||
# returns the offset of the next message
|
||||
def write(self, rom, index, offset):
|
||||
|
||||
# construct the table entry
|
||||
id_bytes = int_to_bytes(self.id, 2)
|
||||
offset_bytes = int_to_bytes(offset, 3)
|
||||
entry = id_bytes + bytes([self.opts, 0x00, 0x07]) + offset_bytes
|
||||
# write it back
|
||||
entry_offset = EXTENDED_TABLE_START + 8 * index
|
||||
rom.write_bytes(entry_offset, entry)
|
||||
|
||||
for code in self.text_codes:
|
||||
offset = code.write(rom, offset)
|
||||
|
||||
while offset % 4 > 0:
|
||||
offset = Text_Code(0x00, 0).write(rom, offset) # pad to 4 byte align
|
||||
|
||||
return offset
|
||||
|
||||
|
||||
def __init__(self, raw_text, index, id, opts, offset, length):
|
||||
|
||||
self.raw_text = raw_text
|
||||
|
||||
self.index = index
|
||||
self.id = id
|
||||
self.opts = opts # Textbox type and y position
|
||||
self.box_type = (self.opts & 0xF0) >> 4
|
||||
self.position = (self.opts & 0x0F)
|
||||
self.offset = offset
|
||||
self.length = length
|
||||
|
||||
self.has_goto = False
|
||||
self.has_keep_open = False
|
||||
self.has_event = False
|
||||
self.has_fade = False
|
||||
self.has_ocarina = False
|
||||
self.has_two_choice = False
|
||||
self.has_three_choice = False
|
||||
self.ending = None
|
||||
|
||||
self.parse_text()
|
||||
|
||||
# read a single message from rom
|
||||
@classmethod
|
||||
def from_rom(cls, rom, index):
|
||||
|
||||
entry_offset = ENG_TABLE_START + 8 * index
|
||||
entry = rom.read_bytes(entry_offset, 8)
|
||||
next = rom.read_bytes(entry_offset + 8, 8)
|
||||
|
||||
id = bytes_to_int(entry[0:2])
|
||||
opts = entry[2]
|
||||
offset = bytes_to_int(entry[5:8])
|
||||
length = bytes_to_int(next[5:8]) - offset
|
||||
|
||||
raw_text = rom.read_bytes(TEXT_START + offset, length)
|
||||
|
||||
return cls(raw_text, index, id, opts, offset, length)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, text, id=0, opts=0x00):
|
||||
bytes = list(text.encode('utf-8')) + [0x02]
|
||||
|
||||
# Clean up garbage values added when encoding special characters again.
|
||||
bytes = list(filter(lambda a: a != 194, bytes)) # 0xC2 added before each accent char.
|
||||
i = 0
|
||||
while i < len(bytes) - 1:
|
||||
if bytes[i] in SPECIAL_CHARACTERS and bytes[i] not in UTF8_TO_OOT_SPECIAL.values(): # This indicates it's one of the button chars (A button, etc).
|
||||
# Have to delete 2 inserted garbage values.
|
||||
del bytes[i-1]
|
||||
del bytes[i-2]
|
||||
i -= 2
|
||||
i+= 1
|
||||
|
||||
return cls(bytes, 0, id, opts, 0, len(bytes) + 1)
|
||||
|
||||
@classmethod
|
||||
def from_bytearray(cls, bytearray, id=0, opts=0x00):
|
||||
bytes = list(bytearray) + [0x02]
|
||||
|
||||
return cls(bytes, 0, id, opts, 0, len(bytes) + 1)
|
||||
|
||||
__str__ = __repr__ = display
|
||||
|
||||
# wrapper for updating the text of a message, given its message id
|
||||
# if the id does not exist in the list, then it will add it
|
||||
def update_message_by_id(messages, id, text, opts=None):
|
||||
# get the message index
|
||||
index = next( (m.index for m in messages if m.id == id), -1)
|
||||
# update if it was found
|
||||
if index >= 0:
|
||||
update_message_by_index(messages, index, text, opts)
|
||||
else:
|
||||
add_message(messages, text, id, opts)
|
||||
|
||||
# Gets the message by its ID. Returns None if the index does not exist
|
||||
def get_message_by_id(messages, id):
|
||||
# get the message index
|
||||
index = next( (m.index for m in messages if m.id == id), -1)
|
||||
if index >= 0:
|
||||
return messages[index]
|
||||
else:
|
||||
return None
|
||||
|
||||
# wrapper for updating the text of a message, given its index in the list
|
||||
def update_message_by_index(messages, index, text, opts=None):
|
||||
if opts is None:
|
||||
opts = messages[index].opts
|
||||
|
||||
if isinstance(text, bytearray):
|
||||
messages[index] = Message.from_bytearray(text, messages[index].id, opts)
|
||||
else:
|
||||
messages[index] = Message.from_string(text, messages[index].id, opts)
|
||||
messages[index].index = index
|
||||
|
||||
# wrapper for adding a string message to a list of messages
|
||||
def add_message(messages, text, id=0, opts=0x00):
|
||||
if isinstance(text, bytearray):
|
||||
messages.append( Message.from_bytearray(text, id, opts) )
|
||||
else:
|
||||
messages.append( Message.from_string(text, id, opts) )
|
||||
messages[-1].index = len(messages) - 1
|
||||
|
||||
# holds a row in the shop item table (which contains pointers to the description and purchase messages)
|
||||
class Shop_Item():
|
||||
|
||||
def display(self):
|
||||
meta_data = ["#" + str(self.index),
|
||||
"Item: 0x" + "{:04x}".format(self.get_item_id),
|
||||
"Price: " + str(self.price),
|
||||
"Amount: " + str(self.pieces),
|
||||
"Object: 0x" + "{:04x}".format(self.object),
|
||||
"Model: 0x" + "{:04x}".format(self.model),
|
||||
"Description: 0x" + "{:04x}".format(self.description_message),
|
||||
"Purchase: 0x" + "{:04x}".format(self.purchase_message),]
|
||||
func_data = [
|
||||
"func1: 0x" + "{:08x}".format(self.func1),
|
||||
"func2: 0x" + "{:08x}".format(self.func2),
|
||||
"func3: 0x" + "{:08x}".format(self.func3),
|
||||
"func4: 0x" + "{:08x}".format(self.func4),]
|
||||
return ', '.join(meta_data) + '\n' + ', '.join(func_data)
|
||||
|
||||
# write the shop item back
|
||||
def write(self, rom, shop_table_address, index):
|
||||
|
||||
entry_offset = shop_table_address + 0x20 * index
|
||||
|
||||
bytes = []
|
||||
bytes += int_to_bytes(self.object, 2)
|
||||
bytes += int_to_bytes(self.model, 2)
|
||||
bytes += int_to_bytes(self.func1, 4)
|
||||
bytes += int_to_bytes(self.price, 2, signed=True)
|
||||
bytes += int_to_bytes(self.pieces, 2)
|
||||
bytes += int_to_bytes(self.description_message, 2)
|
||||
bytes += int_to_bytes(self.purchase_message, 2)
|
||||
bytes += [0x00, 0x00]
|
||||
bytes += int_to_bytes(self.get_item_id, 2)
|
||||
bytes += int_to_bytes(self.func2, 4)
|
||||
bytes += int_to_bytes(self.func3, 4)
|
||||
bytes += int_to_bytes(self.func4, 4)
|
||||
|
||||
rom.write_bytes(entry_offset, bytes)
|
||||
|
||||
# read a single message
|
||||
def __init__(self, rom, shop_table_address, index):
|
||||
|
||||
entry_offset = shop_table_address + 0x20 * index
|
||||
entry = rom.read_bytes(entry_offset, 0x20)
|
||||
|
||||
self.index = index
|
||||
self.object = bytes_to_int(entry[0x00:0x02])
|
||||
self.model = bytes_to_int(entry[0x02:0x04])
|
||||
self.func1 = bytes_to_int(entry[0x04:0x08])
|
||||
self.price = bytes_to_int(entry[0x08:0x0A])
|
||||
self.pieces = bytes_to_int(entry[0x0A:0x0C])
|
||||
self.description_message = bytes_to_int(entry[0x0C:0x0E])
|
||||
self.purchase_message = bytes_to_int(entry[0x0E:0x10])
|
||||
# 0x10-0x11 is always 0000 padded apparently
|
||||
self.get_item_id = bytes_to_int(entry[0x12:0x14])
|
||||
self.func2 = bytes_to_int(entry[0x14:0x18])
|
||||
self.func3 = bytes_to_int(entry[0x18:0x1C])
|
||||
self.func4 = bytes_to_int(entry[0x1C:0x20])
|
||||
|
||||
__str__ = __repr__ = display
|
||||
|
||||
# reads each of the shop items
|
||||
def read_shop_items(rom, shop_table_address):
|
||||
shop_items = []
|
||||
|
||||
for index in range(0, 100):
|
||||
shop_items.append( Shop_Item(rom, shop_table_address, index) )
|
||||
|
||||
return shop_items
|
||||
|
||||
# writes each of the shop item back into rom
|
||||
def write_shop_items(rom, shop_table_address, shop_items):
|
||||
for s in shop_items:
|
||||
s.write(rom, shop_table_address, s.index)
|
||||
|
||||
# these are unused shop items, and contain text ids that are used elsewhere, and should not be moved
|
||||
SHOP_ITEM_EXCEPTIONS = [0x0A, 0x0B, 0x11, 0x12, 0x13, 0x14, 0x29]
|
||||
|
||||
# returns a set of all message ids used for shop items
|
||||
def get_shop_message_id_set(shop_items):
|
||||
ids = set()
|
||||
for shop in shop_items:
|
||||
if shop.index not in SHOP_ITEM_EXCEPTIONS:
|
||||
ids.add(shop.description_message)
|
||||
ids.add(shop.purchase_message)
|
||||
return ids
|
||||
|
||||
# remove all messages that easy to tell are unused to create space in the message index table
|
||||
def remove_unused_messages(messages):
|
||||
messages[:] = [m for m in messages if not m.is_id_message()]
|
||||
for index, m in enumerate(messages):
|
||||
m.index = index
|
||||
|
||||
# takes all messages used for shop items, and moves messages from the 00xx range into the unused 80xx range
|
||||
def move_shop_item_messages(messages, shop_items):
|
||||
# checks if a message id is in the item message range
|
||||
def is_in_item_range(id):
|
||||
bytes = int_to_bytes(id, 2)
|
||||
return bytes[0] == 0x00
|
||||
# get the ids we want to move
|
||||
ids = set( id for id in get_shop_message_id_set(shop_items) if is_in_item_range(id) )
|
||||
# update them in the message list
|
||||
for id in ids:
|
||||
# should be a singleton list, but in case something funky is going on, handle it as a list regardless
|
||||
relevant_messages = [message for message in messages if message.id == id]
|
||||
if len(relevant_messages) >= 2:
|
||||
raise(TypeError("duplicate id in move_shop_item_messages"))
|
||||
|
||||
for message in relevant_messages:
|
||||
message.id |= 0x8000
|
||||
# update them in the shop item list
|
||||
for shop in shop_items:
|
||||
if is_in_item_range(shop.description_message):
|
||||
shop.description_message |= 0x8000
|
||||
if is_in_item_range(shop.purchase_message):
|
||||
shop.purchase_message |= 0x8000
|
||||
|
||||
def make_player_message(text):
|
||||
player_text = '\x05\x42\x0F\x05\x40'
|
||||
pronoun_mapping = {
|
||||
"You have ": player_text + " ",
|
||||
"You are ": player_text + " is ",
|
||||
"You've ": player_text + " ",
|
||||
"Your ": player_text + "'s ",
|
||||
"You ": player_text + " ",
|
||||
|
||||
"you have ": player_text + " ",
|
||||
"you are ": player_text + " is ",
|
||||
"you've ": player_text + " ",
|
||||
"your ": player_text + "'s ",
|
||||
"you ": player_text + " ",
|
||||
}
|
||||
|
||||
verb_mapping = {
|
||||
'obtained ': 'got ',
|
||||
'received ': 'got ',
|
||||
'learned ': 'got ',
|
||||
'borrowed ': 'got ',
|
||||
'found ': 'got ',
|
||||
}
|
||||
|
||||
new_text = text
|
||||
|
||||
# Replace the first instance of a 'You' with the player name
|
||||
lower_text = text.lower()
|
||||
you_index = lower_text.find('you')
|
||||
if you_index != -1:
|
||||
for find_text, replace_text in pronoun_mapping.items():
|
||||
# if the index do not match, then it is not the first 'You'
|
||||
if text.find(find_text) == you_index:
|
||||
new_text = new_text.replace(find_text, replace_text, 1)
|
||||
break
|
||||
|
||||
# because names are longer, we shorten the verbs to they fit in the textboxes better
|
||||
for find_text, replace_text in verb_mapping.items():
|
||||
new_text = new_text.replace(find_text, replace_text)
|
||||
|
||||
wrapped_text = line_wrap(new_text, False, False, False)
|
||||
if wrapped_text != new_text:
|
||||
new_text = line_wrap(new_text, True, True, False)
|
||||
|
||||
return new_text
|
||||
|
||||
|
||||
# reduce item message sizes and add new item messages
|
||||
# make sure to call this AFTER move_shop_item_messages()
|
||||
def update_item_messages(messages, world):
|
||||
new_item_messages = {**ITEM_MESSAGES, **KEYSANITY_MESSAGES}
|
||||
for id, text in new_item_messages.items():
|
||||
if len(world.world.worlds) > 1:
|
||||
update_message_by_id(messages, id, make_player_message(text), 0x23)
|
||||
else:
|
||||
update_message_by_id(messages, id, text, 0x23)
|
||||
|
||||
for id, (text, opt) in MISC_MESSAGES.items():
|
||||
update_message_by_id(messages, id, text, opt)
|
||||
|
||||
|
||||
# run all keysanity related patching to add messages for dungeon specific items
|
||||
def add_item_messages(messages, shop_items, world):
|
||||
move_shop_item_messages(messages, shop_items)
|
||||
update_item_messages(messages, world)
|
||||
|
||||
|
||||
# reads each of the game's messages into a list of Message objects
|
||||
def read_messages(rom):
|
||||
table_offset = ENG_TABLE_START
|
||||
index = 0
|
||||
messages = []
|
||||
while True:
|
||||
entry = rom.read_bytes(table_offset, 8)
|
||||
id = bytes_to_int(entry[0:2])
|
||||
|
||||
if id == 0xFFFD:
|
||||
table_offset += 8
|
||||
continue # this is only here to give an ending offset
|
||||
if id == 0xFFFF:
|
||||
break # this marks the end of the table
|
||||
|
||||
messages.append( Message.from_rom(rom, index) )
|
||||
|
||||
index += 1
|
||||
table_offset += 8
|
||||
|
||||
return messages
|
||||
|
||||
# write the messages back
|
||||
def repack_messages(rom, messages, permutation=None, always_allow_skip=True, speed_up_text=True):
|
||||
|
||||
rom.update_dmadata_record(TEXT_START, TEXT_START, TEXT_START + ENG_TEXT_SIZE_LIMIT)
|
||||
|
||||
if permutation is None:
|
||||
permutation = range(len(messages))
|
||||
|
||||
# repack messages
|
||||
offset = 0
|
||||
text_size_limit = ENG_TEXT_SIZE_LIMIT
|
||||
|
||||
for old_index, new_index in enumerate(permutation):
|
||||
old_message = messages[old_index]
|
||||
new_message = messages[new_index]
|
||||
remember_id = new_message.id
|
||||
new_message.id = old_message.id
|
||||
|
||||
# modify message, making it represent how we want it to be written
|
||||
new_message.transform(True, old_message.ending, always_allow_skip, speed_up_text)
|
||||
|
||||
# actually write the message
|
||||
offset = new_message.write(rom, old_index, offset)
|
||||
|
||||
new_message.id = remember_id
|
||||
|
||||
# raise an exception if too much is written
|
||||
# we raise it at the end so that we know how much overflow there is
|
||||
if offset > text_size_limit:
|
||||
raise(TypeError("Message Text table is too large: 0x" + "{:x}".format(offset) + " written / 0x" + "{:x}".format(ENG_TEXT_SIZE_LIMIT) + " allowed."))
|
||||
|
||||
# end the table
|
||||
table_index = len(messages)
|
||||
entry = bytes([0xFF, 0xFD, 0x00, 0x00, 0x07]) + int_to_bytes(offset, 3)
|
||||
entry_offset = EXTENDED_TABLE_START + 8 * table_index
|
||||
rom.write_bytes(entry_offset, entry)
|
||||
table_index += 1
|
||||
entry_offset = EXTENDED_TABLE_START + 8 * table_index
|
||||
if 8 * (table_index + 1) > EXTENDED_TABLE_SIZE:
|
||||
raise(TypeError("Message ID table is too large: 0x" + "{:x}".format(8 * (table_index + 1)) + " written / 0x" + "{:x}".format(EXTENDED_TABLE_SIZE) + " allowed."))
|
||||
rom.write_bytes(entry_offset, [0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
|
||||
|
||||
# shuffles the messages in the game, making sure to keep various message types in their own group
|
||||
def shuffle_messages(messages, except_hints=True, always_allow_skip=True):
|
||||
|
||||
permutation = [i for i, _ in enumerate(messages)]
|
||||
|
||||
def is_exempt(m):
|
||||
hint_ids = (
|
||||
GOSSIP_STONE_MESSAGES + TEMPLE_HINTS_MESSAGES + LIGHT_ARROW_HINT +
|
||||
list(KEYSANITY_MESSAGES.keys()) + shuffle_messages.shop_item_messages +
|
||||
shuffle_messages.scrubs_message_ids +
|
||||
[0x5036, 0x70F5] # Chicken count and poe count respectively
|
||||
)
|
||||
shuffle_exempt = [
|
||||
0x208D, # "One more lap!" for Cow in House race.
|
||||
]
|
||||
is_hint = (except_hints and m.id in hint_ids)
|
||||
is_error_message = (m.id == ERROR_MESSAGE)
|
||||
is_shuffle_exempt = (m.id in shuffle_exempt)
|
||||
return (is_hint or is_error_message or m.is_id_message() or is_shuffle_exempt)
|
||||
|
||||
have_goto = list( filter(lambda m: not is_exempt(m) and m.has_goto, messages) )
|
||||
have_keep_open = list( filter(lambda m: not is_exempt(m) and m.has_keep_open, messages) )
|
||||
have_event = list( filter(lambda m: not is_exempt(m) and m.has_event, messages) )
|
||||
have_fade = list( filter(lambda m: not is_exempt(m) and m.has_fade, messages) )
|
||||
have_ocarina = list( filter(lambda m: not is_exempt(m) and m.has_ocarina, messages) )
|
||||
have_two_choice = list( filter(lambda m: not is_exempt(m) and m.has_two_choice, messages) )
|
||||
have_three_choice = list( filter(lambda m: not is_exempt(m) and m.has_three_choice, messages) )
|
||||
basic_messages = list( filter(lambda m: not is_exempt(m) and m.is_basic(), messages) )
|
||||
|
||||
|
||||
def shuffle_group(group):
|
||||
group_permutation = [i for i, _ in enumerate(group)]
|
||||
random.shuffle(group_permutation)
|
||||
|
||||
for index_from, index_to in enumerate(group_permutation):
|
||||
permutation[group[index_to].index] = group[index_from].index
|
||||
|
||||
# need to use 'list' to force 'map' to actually run through
|
||||
list( map( shuffle_group, [
|
||||
have_goto + have_keep_open + have_event + have_fade + basic_messages,
|
||||
have_ocarina,
|
||||
have_two_choice,
|
||||
have_three_choice,
|
||||
]))
|
||||
|
||||
return permutation
|
||||
484
worlds/oot/Music.py
Normal file
484
worlds/oot/Music.py
Normal file
@@ -0,0 +1,484 @@
|
||||
#Much of this is heavily inspired from and/or based on az64's / Deathbasket's MM randomizer
|
||||
|
||||
import random
|
||||
import os
|
||||
from .Utils import compare_version, data_path
|
||||
|
||||
|
||||
# Format: (Title, Sequence ID)
|
||||
bgm_sequence_ids = [
|
||||
("Hyrule Field", 0x02),
|
||||
("Dodongos Cavern", 0x18),
|
||||
("Kakariko Adult", 0x19),
|
||||
("Battle", 0x1A),
|
||||
("Boss Battle", 0x1B),
|
||||
("Inside Deku Tree", 0x1C),
|
||||
("Market", 0x1D),
|
||||
("Title Theme", 0x1E),
|
||||
("House", 0x1F),
|
||||
("Jabu Jabu", 0x26),
|
||||
("Kakariko Child", 0x27),
|
||||
("Fairy Fountain", 0x28),
|
||||
("Zelda Theme", 0x29),
|
||||
("Fire Temple", 0x2A),
|
||||
("Forest Temple", 0x2C),
|
||||
("Castle Courtyard", 0x2D),
|
||||
("Ganondorf Theme", 0x2E),
|
||||
("Lon Lon Ranch", 0x2F),
|
||||
("Goron City", 0x30),
|
||||
("Miniboss Battle", 0x38),
|
||||
("Temple of Time", 0x3A),
|
||||
("Kokiri Forest", 0x3C),
|
||||
("Lost Woods", 0x3E),
|
||||
("Spirit Temple", 0x3F),
|
||||
("Horse Race", 0x40),
|
||||
("Ingo Theme", 0x42),
|
||||
("Fairy Flying", 0x4A),
|
||||
("Deku Tree", 0x4B),
|
||||
("Windmill Hut", 0x4C),
|
||||
("Shooting Gallery", 0x4E),
|
||||
("Sheik Theme", 0x4F),
|
||||
("Zoras Domain", 0x50),
|
||||
("Shop", 0x55),
|
||||
("Chamber of the Sages", 0x56),
|
||||
("Ice Cavern", 0x58),
|
||||
("Kaepora Gaebora", 0x5A),
|
||||
("Shadow Temple", 0x5B),
|
||||
("Water Temple", 0x5C),
|
||||
("Gerudo Valley", 0x5F),
|
||||
("Potion Shop", 0x60),
|
||||
("Kotake and Koume", 0x61),
|
||||
("Castle Escape", 0x62),
|
||||
("Castle Underground", 0x63),
|
||||
("Ganondorf Battle", 0x64),
|
||||
("Ganon Battle", 0x65),
|
||||
("Fire Boss", 0x6B),
|
||||
("Mini-game", 0x6C)
|
||||
]
|
||||
|
||||
fanfare_sequence_ids = [
|
||||
("Game Over", 0x20),
|
||||
("Boss Defeated", 0x21),
|
||||
("Item Get", 0x22),
|
||||
("Ganondorf Appears", 0x23),
|
||||
("Heart Container Get", 0x24),
|
||||
("Treasure Chest", 0x2B),
|
||||
("Spirit Stone Get", 0x32),
|
||||
("Heart Piece Get", 0x39),
|
||||
("Escape from Ranch", 0x3B),
|
||||
("Learn Song", 0x3D),
|
||||
("Epona Race Goal", 0x41),
|
||||
("Medallion Get", 0x43),
|
||||
("Zelda Turns Around", 0x51),
|
||||
("Master Sword", 0x53),
|
||||
("Door of Time", 0x59)
|
||||
]
|
||||
|
||||
ocarina_sequence_ids = [
|
||||
("Prelude of Light", 0x25),
|
||||
("Bolero of Fire", 0x33),
|
||||
("Minuet of Forest", 0x34),
|
||||
("Serenade of Water", 0x35),
|
||||
("Requiem of Spirit", 0x36),
|
||||
("Nocturne of Shadow", 0x37),
|
||||
("Saria's Song", 0x44),
|
||||
("Epona's Song", 0x45),
|
||||
("Zelda's Lullaby", 0x46),
|
||||
("Sun's Song", 0x47),
|
||||
("Song of Time", 0x48),
|
||||
("Song of Storms", 0x49)
|
||||
]
|
||||
|
||||
# Represents the information associated with a sequence, aside from the sequence data itself
|
||||
class TableEntry(object):
|
||||
def __init__(self, name, cosmetic_name, type = 0x0202, instrument_set = 0x03, replaces = -1, vanilla_id = -1):
|
||||
self.name = name
|
||||
self.cosmetic_name = cosmetic_name
|
||||
self.replaces = replaces
|
||||
self.vanilla_id = vanilla_id
|
||||
self.type = type
|
||||
self.instrument_set = instrument_set
|
||||
|
||||
|
||||
def copy(self):
|
||||
copy = TableEntry(self.name, self.cosmetic_name, self.type, self.instrument_set, self.replaces, self.vanilla_id)
|
||||
return copy
|
||||
|
||||
|
||||
# Represents actual sequence data, along with metadata for the sequence data block
|
||||
class Sequence(object):
|
||||
def __init__(self):
|
||||
self.address = -1
|
||||
self.size = -1
|
||||
self.data = []
|
||||
|
||||
|
||||
def process_sequences(rom, sequences, target_sequences, disabled_source_sequences, disabled_target_sequences, ids, seq_type = 'bgm'):
|
||||
# Process vanilla music data
|
||||
for bgm in ids:
|
||||
# Get sequence metadata
|
||||
name = bgm[0]
|
||||
cosmetic_name = name
|
||||
type = rom.read_int16(0xB89AE8 + (bgm[1] * 0x10))
|
||||
instrument_set = rom.read_byte(0xB89911 + 0xDD + (bgm[1] * 2))
|
||||
id = bgm[1]
|
||||
|
||||
# Create new sequences
|
||||
seq = TableEntry(name, cosmetic_name, type, instrument_set, vanilla_id = id)
|
||||
target = TableEntry(name, cosmetic_name, type, instrument_set, replaces = id)
|
||||
|
||||
# Special handling for file select/fairy fountain
|
||||
if seq.vanilla_id != 0x57 and cosmetic_name not in disabled_source_sequences:
|
||||
sequences.append(seq)
|
||||
if cosmetic_name not in disabled_target_sequences:
|
||||
target_sequences.append(target)
|
||||
|
||||
# If present, load the file containing custom music to exclude
|
||||
try:
|
||||
with open(os.path.join(data_path(), u'custom_music_exclusion.txt')) as excl_in:
|
||||
seq_exclusion_list = excl_in.readlines()
|
||||
seq_exclusion_list = [seq.rstrip() for seq in seq_exclusion_list if seq[0] != '#']
|
||||
seq_exclusion_list = [seq for seq in seq_exclusion_list if seq.endswith('.meta')]
|
||||
except FileNotFoundError:
|
||||
seq_exclusion_list = []
|
||||
|
||||
# Process music data in data/Music/
|
||||
# Each sequence requires a valid .seq sequence file and a .meta metadata file
|
||||
# Current .meta format: Cosmetic Name\nInstrument Set\nPool
|
||||
for dirpath, _, filenames in os.walk(u'./data/Music', followlinks=True):
|
||||
for fname in filenames:
|
||||
# Skip if included in exclusion file
|
||||
if fname in seq_exclusion_list:
|
||||
continue
|
||||
|
||||
# Find meta file and check if corresponding seq file exists
|
||||
if fname.endswith('.meta') and os.path.isfile(os.path.join(dirpath, fname.split('.')[0] + '.seq')):
|
||||
# Read meta info
|
||||
try:
|
||||
with open(os.path.join(dirpath, fname), 'r') as stream:
|
||||
lines = stream.readlines()
|
||||
# Strip newline(s)
|
||||
lines = [line.rstrip() for line in lines]
|
||||
except FileNotFoundError as ex:
|
||||
raise FileNotFoundError('No meta file for: "' + fname + '". This should never happen')
|
||||
|
||||
# Create new sequence, checking third line for correct type
|
||||
if (len(lines) > 2 and (lines[2].lower() == seq_type.lower() or lines[2] == '')) or (len(lines) <= 2 and seq_type == 'bgm'):
|
||||
seq = TableEntry(os.path.join(dirpath, fname.split('.')[0]), lines[0], instrument_set = int(lines[1], 16))
|
||||
|
||||
if seq.instrument_set < 0x00 or seq.instrument_set > 0x25:
|
||||
raise Exception('Sequence instrument must be in range [0x00, 0x25]')
|
||||
|
||||
if seq.cosmetic_name not in disabled_source_sequences:
|
||||
sequences.append(seq)
|
||||
|
||||
return sequences, target_sequences
|
||||
|
||||
|
||||
def shuffle_music(sequences, target_sequences, music_mapping, log):
|
||||
sequence_dict = {}
|
||||
sequence_ids = []
|
||||
|
||||
for sequence in sequences:
|
||||
if sequence.cosmetic_name == "None":
|
||||
raise Exception('Sequences should not be named "None" as that is used for disabled music. Sequence with improper name: %s' % sequence.name)
|
||||
if sequence.cosmetic_name in sequence_dict:
|
||||
raise Exception('Sequence names should be unique. Duplicate sequence name: %s' % sequence.cosmetic_name)
|
||||
sequence_dict[sequence.cosmetic_name] = sequence
|
||||
if sequence.cosmetic_name not in music_mapping.values():
|
||||
sequence_ids.append(sequence.cosmetic_name)
|
||||
|
||||
# Shuffle the sequences
|
||||
if len(sequences) < len(target_sequences):
|
||||
raise Exception(f"Not enough custom music/fanfares ({len(sequences)}) to omit base Ocarina of Time sequences ({len(target_sequences)}).")
|
||||
random.shuffle(sequence_ids)
|
||||
|
||||
sequences = []
|
||||
for target_sequence in target_sequences:
|
||||
sequence = sequence_dict[sequence_ids.pop()].copy() if target_sequence.cosmetic_name not in music_mapping \
|
||||
else ("None", 0x0) if music_mapping[target_sequence.cosmetic_name] == "None" \
|
||||
else sequence_dict[music_mapping[target_sequence.cosmetic_name]].copy()
|
||||
sequences.append(sequence)
|
||||
sequence.replaces = target_sequence.replaces
|
||||
log[target_sequence.cosmetic_name] = sequence.cosmetic_name
|
||||
|
||||
return sequences, log
|
||||
|
||||
|
||||
def rebuild_sequences(rom, sequences):
|
||||
# List of sequences (actual sequence data objects) containing the vanilla sequence data
|
||||
old_sequences = []
|
||||
|
||||
for i in range(0x6E):
|
||||
# Create new sequence object, an entry for the audio sequence
|
||||
entry = Sequence()
|
||||
# Get the address for the entry's pointer table entry
|
||||
entry_address = 0xB89AE0 + (i * 0x10)
|
||||
# Extract the info from the pointer table entry
|
||||
entry.address = rom.read_int32(entry_address)
|
||||
entry.size = rom.read_int32(entry_address + 0x04)
|
||||
|
||||
# If size > 0, read the sequence data from the rom into the sequence object
|
||||
if entry.size > 0:
|
||||
entry.data = rom.read_bytes(entry.address + 0x029DE0, entry.size)
|
||||
else:
|
||||
s = [seq for seq in sequences if seq.replaces == i]
|
||||
if s != [] and entry.address > 0 and entry.address < 128:
|
||||
s = s.pop()
|
||||
if s.replaces != 0x28:
|
||||
s.replaces = entry.address
|
||||
else:
|
||||
# Special handling for file select/fairy fountain
|
||||
entry.data = old_sequences[0x57].data
|
||||
entry.size = old_sequences[0x57].size
|
||||
|
||||
old_sequences.append(entry)
|
||||
|
||||
# List of sequences containing the new sequence data
|
||||
new_sequences = []
|
||||
address = 0
|
||||
# Byte array to hold the data for the whole audio sequence
|
||||
new_audio_sequence = []
|
||||
|
||||
for i in range(0x6E):
|
||||
new_entry = Sequence()
|
||||
# If sequence size is 0, the address doesn't matter and it doesn't effect the current address
|
||||
if old_sequences[i].size == 0:
|
||||
new_entry.address = old_sequences[i].address
|
||||
# Continue from the end of the new sequence table
|
||||
else:
|
||||
new_entry.address = address
|
||||
|
||||
s = [seq for seq in sequences if seq.replaces == i]
|
||||
if s != []:
|
||||
assert len(s) == 1
|
||||
s = s.pop()
|
||||
# If we are using a vanilla sequence, get its data from old_sequences
|
||||
if s.vanilla_id != -1:
|
||||
new_entry.size = old_sequences[s.vanilla_id].size
|
||||
new_entry.data = old_sequences[s.vanilla_id].data
|
||||
else:
|
||||
# Read sequence info
|
||||
try:
|
||||
with open(s.name + '.seq', 'rb') as stream:
|
||||
new_entry.data = bytearray(stream.read())
|
||||
new_entry.size = len(new_entry.data)
|
||||
if new_entry.size <= 0x10:
|
||||
raise Exception('Invalid sequence file "' + s.name + '.seq"')
|
||||
new_entry.data[1] = 0x20
|
||||
except FileNotFoundError as ex:
|
||||
raise FileNotFoundError('No sequence file for: "' + s.name + '"')
|
||||
else:
|
||||
new_entry.size = old_sequences[i].size
|
||||
new_entry.data = old_sequences[i].data
|
||||
|
||||
new_sequences.append(new_entry)
|
||||
|
||||
# Concatenate the full audio sequence and the new sequence data
|
||||
if new_entry.data != [] and new_entry.size > 0:
|
||||
# Align sequences to 0x10
|
||||
if new_entry.size % 0x10 != 0:
|
||||
new_entry.data.extend(bytearray(0x10 - (new_entry.size % 0x10)))
|
||||
new_entry.size += 0x10 - (new_entry.size % 0x10)
|
||||
new_audio_sequence.extend(new_entry.data)
|
||||
# Increment the current address by the size of the new sequence
|
||||
address += new_entry.size
|
||||
|
||||
# Check if the new audio sequence is larger than the vanilla one
|
||||
if address > 0x04F690:
|
||||
# Zero out the old audio sequence
|
||||
rom.buffer[0x029DE0 : 0x029DE0 + 0x04F690] = [0] * 0x04F690
|
||||
|
||||
# Append new audio sequence
|
||||
new_address = rom.free_space()
|
||||
rom.write_bytes(new_address, new_audio_sequence)
|
||||
|
||||
#Update dmatable
|
||||
rom.update_dmadata_record(0x029DE0, new_address, new_address + address)
|
||||
|
||||
else:
|
||||
# Write new audio sequence file
|
||||
rom.write_bytes(0x029DE0, new_audio_sequence)
|
||||
|
||||
# Update pointer table
|
||||
for i in range(0x6E):
|
||||
rom.write_int32(0xB89AE0 + (i * 0x10), new_sequences[i].address)
|
||||
rom.write_int32(0xB89AE0 + (i * 0x10) + 0x04, new_sequences[i].size)
|
||||
s = [seq for seq in sequences if seq.replaces == i]
|
||||
if s != []:
|
||||
assert len(s) == 1
|
||||
s = s.pop()
|
||||
rom.write_int16(0xB89AE0 + (i * 0x10) + 0x08, s.type)
|
||||
|
||||
# Update instrument sets
|
||||
for i in range(0x6E):
|
||||
base = 0xB89911 + 0xDD + (i * 2)
|
||||
j = -1
|
||||
if new_sequences[i].size == 0:
|
||||
try:
|
||||
j = [seq for seq in sequences if seq.replaces == new_sequences[i].address].pop()
|
||||
except:
|
||||
j = -1
|
||||
else:
|
||||
try:
|
||||
j = [seq for seq in sequences if seq.replaces == i].pop()
|
||||
except:
|
||||
j = -1
|
||||
if j != -1:
|
||||
rom.write_byte(base, j.instrument_set)
|
||||
|
||||
|
||||
def shuffle_pointers_table(rom, ids, music_mapping, log):
|
||||
# Read in all the Music data
|
||||
bgm_data = {}
|
||||
bgm_ids = []
|
||||
|
||||
for bgm in ids:
|
||||
bgm_sequence = rom.read_bytes(0xB89AE0 + (bgm[1] * 0x10), 0x10)
|
||||
bgm_instrument = rom.read_int16(0xB89910 + 0xDD + (bgm[1] * 2))
|
||||
bgm_data[bgm[0]] = (bgm[0], bgm_sequence, bgm_instrument)
|
||||
if bgm[0] not in music_mapping.values():
|
||||
bgm_ids.append(bgm[0])
|
||||
|
||||
# shuffle data
|
||||
random.shuffle(bgm_ids)
|
||||
|
||||
# Write Music data back in random ordering
|
||||
for bgm in ids:
|
||||
if bgm[0] in music_mapping and music_mapping[bgm[0]] in bgm_data:
|
||||
bgm_name = music_mapping[bgm[0]]
|
||||
else:
|
||||
bgm_name = bgm_ids.pop()
|
||||
bgm_name, bgm_sequence, bgm_instrument = bgm_data[bgm_name]
|
||||
rom.write_bytes(0xB89AE0 + (bgm[1] * 0x10), bgm_sequence)
|
||||
rom.write_int16(0xB89910 + 0xDD + (bgm[1] * 2), bgm_instrument)
|
||||
log[bgm[0]] = bgm_name
|
||||
|
||||
# Write Fairy Fountain instrument to File Select (uses same track but different instrument set pointer for some reason)
|
||||
rom.write_int16(0xB89910 + 0xDD + (0x57 * 2), rom.read_int16(0xB89910 + 0xDD + (0x28 * 2)))
|
||||
return log
|
||||
|
||||
|
||||
def randomize_music(rom, ootworld, music_mapping):
|
||||
log = {}
|
||||
errors = []
|
||||
sequences = []
|
||||
target_sequences = []
|
||||
fanfare_sequences = []
|
||||
fanfare_target_sequences = []
|
||||
disabled_source_sequences = {}
|
||||
disabled_target_sequences = {}
|
||||
|
||||
# Make sure we aren't operating directly on these.
|
||||
music_mapping = music_mapping.copy()
|
||||
bgm_ids = bgm_sequence_ids.copy()
|
||||
ff_ids = fanfare_sequence_ids.copy()
|
||||
|
||||
# Check if we have mapped music for BGM, Fanfares, or Ocarina Fanfares
|
||||
bgm_mapped = any(bgm[0] in music_mapping for bgm in bgm_ids)
|
||||
ff_mapped = any(ff[0] in music_mapping for ff in ff_ids)
|
||||
ocarina_mapped = any(ocarina[0] in music_mapping for ocarina in ocarina_sequence_ids)
|
||||
|
||||
# Include ocarina songs in fanfare pool if checked
|
||||
if ootworld.ocarina_fanfares or ocarina_mapped:
|
||||
ff_ids.extend(ocarina_sequence_ids)
|
||||
|
||||
# Flag sequence locations that are set to off for disabling.
|
||||
disabled_ids = []
|
||||
if ootworld.background_music == 'off':
|
||||
disabled_ids += [music_id for music_id in bgm_ids]
|
||||
if ootworld.fanfares == 'off':
|
||||
disabled_ids += [music_id for music_id in ff_ids]
|
||||
disabled_ids += [music_id for music_id in ocarina_sequence_ids]
|
||||
for bgm in [music_id for music_id in bgm_ids + ff_ids + ocarina_sequence_ids]:
|
||||
if music_mapping.get(bgm[0], '') == "None":
|
||||
disabled_target_sequences[bgm[0]] = bgm
|
||||
for bgm in disabled_ids:
|
||||
if bgm[0] not in music_mapping:
|
||||
music_mapping[bgm[0]] = "None"
|
||||
disabled_target_sequences[bgm[0]] = bgm
|
||||
|
||||
# Map music to itself if music is set to normal.
|
||||
normal_ids = []
|
||||
if ootworld.background_music == 'normal' and bgm_mapped:
|
||||
normal_ids += [music_id for music_id in bgm_ids]
|
||||
if ootworld.fanfares == 'normal' and (ff_mapped or ocarina_mapped):
|
||||
normal_ids += [music_id for music_id in ff_ids]
|
||||
if not ootworld.ocarina_fanfares and ootworld.fanfares == 'normal' and ocarina_mapped:
|
||||
normal_ids += [music_id for music_id in ocarina_sequence_ids]
|
||||
for bgm in normal_ids:
|
||||
if bgm[0] not in music_mapping:
|
||||
music_mapping[bgm[0]] = bgm[0]
|
||||
|
||||
# If not creating patch file, shuffle audio sequences. Otherwise, shuffle pointer table
|
||||
# If generating from patch, also do a version check to make sure custom sequences are supported.
|
||||
# custom_sequences_enabled = ootworld.compress_rom != 'Patch'
|
||||
# if ootworld.patch_file != '':
|
||||
# rom_version_bytes = rom.read_bytes(0x35, 3)
|
||||
# rom_version = f'{rom_version_bytes[0]}.{rom_version_bytes[1]}.{rom_version_bytes[2]}'
|
||||
# if compare_version(rom_version, '4.11.13') < 0:
|
||||
# errors.append("Custom music is not supported by this patch version. Only randomizing vanilla music.")
|
||||
# custom_sequences_enabled = False
|
||||
# if custom_sequences_enabled:
|
||||
# if ootworld.background_music in ['random', 'random_custom_only'] or bgm_mapped:
|
||||
# process_sequences(rom, sequences, target_sequences, disabled_source_sequences, disabled_target_sequences, bgm_ids)
|
||||
# if ootworld.background_music == 'random_custom_only':
|
||||
# sequences = [seq for seq in sequences if seq.cosmetic_name not in [x[0] for x in bgm_ids] or seq.cosmetic_name in music_mapping.values()]
|
||||
# sequences, log = shuffle_music(sequences, target_sequences, music_mapping, log)
|
||||
|
||||
# if ootworld.fanfares in ['random', 'random_custom_only'] or ff_mapped or ocarina_mapped:
|
||||
# process_sequences(rom, fanfare_sequences, fanfare_target_sequences, disabled_source_sequences, disabled_target_sequences, ff_ids, 'fanfare')
|
||||
# if ootworld.fanfares == 'random_custom_only':
|
||||
# fanfare_sequences = [seq for seq in fanfare_sequences if seq.cosmetic_name not in [x[0] for x in fanfare_sequence_ids] or seq.cosmetic_name in music_mapping.values()]
|
||||
# fanfare_sequences, log = shuffle_music(fanfare_sequences, fanfare_target_sequences, music_mapping, log)
|
||||
|
||||
# if disabled_source_sequences:
|
||||
# log = disable_music(rom, disabled_source_sequences.values(), log)
|
||||
|
||||
# rebuild_sequences(rom, sequences + fanfare_sequences)
|
||||
# else:
|
||||
if ootworld.background_music == 'randomized' or bgm_mapped:
|
||||
log = shuffle_pointers_table(rom, bgm_ids, music_mapping, log)
|
||||
|
||||
if ootworld.fanfares == 'randomized' or ff_mapped or ocarina_mapped:
|
||||
log = shuffle_pointers_table(rom, ff_ids, music_mapping, log)
|
||||
# end_else
|
||||
if disabled_target_sequences:
|
||||
log = disable_music(rom, disabled_target_sequences.values(), log)
|
||||
|
||||
return log, errors
|
||||
|
||||
|
||||
def disable_music(rom, ids, log):
|
||||
# First track is no music
|
||||
blank_track = rom.read_bytes(0xB89AE0 + (0 * 0x10), 0x10)
|
||||
for bgm in ids:
|
||||
rom.write_bytes(0xB89AE0 + (bgm[1] * 0x10), blank_track)
|
||||
log[bgm[0]] = "None"
|
||||
|
||||
return log
|
||||
|
||||
|
||||
def restore_music(rom):
|
||||
# Restore all music from original
|
||||
for bgm in bgm_sequence_ids + fanfare_sequence_ids + ocarina_sequence_ids:
|
||||
bgm_sequence = rom.original.read_bytes(0xB89AE0 + (bgm[1] * 0x10), 0x10)
|
||||
rom.write_bytes(0xB89AE0 + (bgm[1] * 0x10), bgm_sequence)
|
||||
bgm_instrument = rom.original.read_int16(0xB89910 + 0xDD + (bgm[1] * 2))
|
||||
rom.write_int16(0xB89910 + 0xDD + (bgm[1] * 2), bgm_instrument)
|
||||
|
||||
# restore file select instrument
|
||||
bgm_instrument = rom.original.read_int16(0xB89910 + 0xDD + (0x57 * 2))
|
||||
rom.write_int16(0xB89910 + 0xDD + (0x57 * 2), bgm_instrument)
|
||||
|
||||
# Rebuild audioseq
|
||||
orig_start, orig_end, orig_size = rom.original._get_dmadata_record(0x7470)
|
||||
rom.write_bytes(orig_start, rom.original.read_bytes(orig_start, orig_size))
|
||||
|
||||
# If Audioseq was relocated
|
||||
start, end, size = rom._get_dmadata_record(0x7470)
|
||||
if start != 0x029DE0:
|
||||
# Zero out old audioseq
|
||||
rom.write_bytes(start, [0] * size)
|
||||
rom.update_dmadata_record(start, orig_start, orig_end)
|
||||
|
||||
271
worlds/oot/N64Patch.py
Normal file
271
worlds/oot/N64Patch.py
Normal file
@@ -0,0 +1,271 @@
|
||||
import struct
|
||||
import random
|
||||
import io
|
||||
import array
|
||||
import zlib
|
||||
import copy
|
||||
import zipfile
|
||||
from .ntype import BigStream
|
||||
|
||||
|
||||
# get the next XOR key. Uses some location in the source rom.
|
||||
# This will skip of 0s, since if we hit a block of 0s, the
|
||||
# patch data will be raw.
|
||||
def key_next(rom, key_address, address_range):
|
||||
key = 0
|
||||
while key == 0:
|
||||
key_address += 1
|
||||
if key_address > address_range[1]:
|
||||
key_address = address_range[0]
|
||||
key = rom.original.buffer[key_address]
|
||||
return key, key_address
|
||||
|
||||
|
||||
# creates a XOR block for the patch. This might break it up into
|
||||
# multiple smaller blocks if there is a concern about the XOR key
|
||||
# or if it is too long.
|
||||
def write_block(rom, xor_address, xor_range, block_start, data, patch_data):
|
||||
new_data = []
|
||||
key_offset = 0
|
||||
continue_block = False
|
||||
|
||||
for b in data:
|
||||
if b == 0:
|
||||
# Leave 0s as 0s. Do not XOR
|
||||
new_data += [0]
|
||||
else:
|
||||
# get the next XOR key
|
||||
key, xor_address = key_next(rom, xor_address, xor_range)
|
||||
|
||||
# if the XOR would result in 0, change the key.
|
||||
# This requires breaking up the block.
|
||||
if b == key:
|
||||
write_block_section(block_start, key_offset, new_data, patch_data, continue_block)
|
||||
new_data = []
|
||||
key_offset = 0
|
||||
continue_block = True
|
||||
|
||||
# search for next safe XOR key
|
||||
while b == key:
|
||||
key_offset += 1
|
||||
key, xor_address = key_next(rom, xor_address, xor_range)
|
||||
# if we aren't able to find one quickly, we may need to break again
|
||||
if key_offset == 0xFF:
|
||||
write_block_section(block_start, key_offset, new_data, patch_data, continue_block)
|
||||
new_data = []
|
||||
key_offset = 0
|
||||
continue_block = True
|
||||
|
||||
# XOR the key with the byte
|
||||
new_data += [b ^ key]
|
||||
|
||||
# Break the block if it's too long
|
||||
if (len(new_data) == 0xFFFF):
|
||||
write_block_section(block_start, key_offset, new_data, patch_data, continue_block)
|
||||
new_data = []
|
||||
key_offset = 0
|
||||
continue_block = True
|
||||
|
||||
# Save the block
|
||||
write_block_section(block_start, key_offset, new_data, patch_data, continue_block)
|
||||
return xor_address
|
||||
|
||||
|
||||
# This saves a sub-block for the XOR block. If it's the first part
|
||||
# then it will include the address to write to. Otherwise it will
|
||||
# have a number of XOR keys to skip and then continue writing after
|
||||
# the previous block
|
||||
def write_block_section(start, key_skip, in_data, patch_data, is_continue):
|
||||
if not is_continue:
|
||||
patch_data.append_int32(start)
|
||||
else:
|
||||
patch_data.append_bytes([0xFF, key_skip])
|
||||
patch_data.append_int16(len(in_data))
|
||||
patch_data.append_bytes(in_data)
|
||||
|
||||
|
||||
# This will create the patch file. Which can be applied to a source rom.
|
||||
# xor_range is the range the XOR key will read from. This range is not
|
||||
# too important, but I tried to choose from a section that didn't really
|
||||
# have big gaps of 0s which we want to avoid.
|
||||
def create_patch_file(rom, file, xor_range=(0x00B8AD30, 0x00F029A0)):
|
||||
dma_start, dma_end = rom.get_dma_table_range()
|
||||
|
||||
# add header
|
||||
patch_data = BigStream([])
|
||||
patch_data.append_bytes(list(map(ord, 'ZPFv1')))
|
||||
patch_data.append_int32(dma_start)
|
||||
patch_data.append_int32(xor_range[0])
|
||||
patch_data.append_int32(xor_range[1])
|
||||
|
||||
# get random xor key. This range is chosen because it generally
|
||||
# doesn't have many sections of 0s
|
||||
xor_address = random.Random().randint(*xor_range)
|
||||
patch_data.append_int32(xor_address)
|
||||
|
||||
new_buffer = copy.copy(rom.original.buffer)
|
||||
|
||||
# write every changed DMA entry
|
||||
for dma_index, (from_file, start, size) in rom.changed_dma.items():
|
||||
patch_data.append_int16(dma_index)
|
||||
patch_data.append_int32(from_file)
|
||||
patch_data.append_int32(start)
|
||||
patch_data.append_int24(size)
|
||||
|
||||
# We don't trust files that have modified DMA to have their
|
||||
# changed addresses tracked correctly, so we invalidate the
|
||||
# entire file
|
||||
for address in range(start, start + size):
|
||||
rom.changed_address[address] = rom.buffer[address]
|
||||
|
||||
# Simulate moving the files to know which addresses have changed
|
||||
if from_file >= 0:
|
||||
old_dma_start, old_dma_end, old_size = rom.original.get_dmadata_record_by_key(from_file)
|
||||
copy_size = min(size, old_size)
|
||||
new_buffer[start:start+copy_size] = rom.original.read_bytes(from_file, copy_size)
|
||||
new_buffer[start+copy_size:start+size] = [0] * (size - copy_size)
|
||||
else:
|
||||
# this is a new file, so we just fill with null data
|
||||
new_buffer[start:start+size] = [0] * size
|
||||
|
||||
# end of DMA entries
|
||||
patch_data.append_int16(0xFFFF)
|
||||
|
||||
# filter down the addresses that will actually need to change.
|
||||
# Make sure to not include any of the DMA table addresses
|
||||
changed_addresses = [address for address,value in rom.changed_address.items() \
|
||||
if (address >= dma_end or address < dma_start) and \
|
||||
(address in rom.force_patch or new_buffer[address] != value)]
|
||||
changed_addresses.sort()
|
||||
|
||||
# Write the address changes. We'll store the data with XOR so that
|
||||
# the patch data won't be raw data from the patched rom.
|
||||
data = []
|
||||
block_start = None
|
||||
BLOCK_HEADER_SIZE = 7 # this is used to break up gaps
|
||||
for address in changed_addresses:
|
||||
# if there's a block to write and there's a gap, write it
|
||||
if block_start:
|
||||
block_end = block_start + len(data) - 1
|
||||
if address > block_end + BLOCK_HEADER_SIZE:
|
||||
xor_address = write_block(rom, xor_address, xor_range, block_start, data, patch_data)
|
||||
data = []
|
||||
block_start = None
|
||||
block_end = None
|
||||
|
||||
# start a new block
|
||||
if not block_start:
|
||||
block_start = address
|
||||
block_end = address - 1
|
||||
|
||||
# save the new data
|
||||
data += rom.buffer[block_end+1:address+1]
|
||||
|
||||
# if there was any left over blocks, write them out
|
||||
if block_start:
|
||||
xor_address = write_block(rom, xor_address, xor_range, block_start, data, patch_data)
|
||||
|
||||
# compress the patch file
|
||||
patch_data = bytes(patch_data.buffer)
|
||||
patch_data = zlib.compress(patch_data)
|
||||
|
||||
# save the patch file
|
||||
with open(file, 'wb') as outfile:
|
||||
outfile.write(patch_data)
|
||||
|
||||
|
||||
# This will apply a patch file to a source rom to generate a patched rom.
|
||||
def apply_patch_file(rom, file, sub_file=None):
|
||||
# load the patch file and decompress
|
||||
if sub_file:
|
||||
with zipfile.ZipFile(file, 'r') as patch_archive:
|
||||
try:
|
||||
with patch_archive.open(sub_file, 'r') as stream:
|
||||
patch_data = stream.read()
|
||||
except KeyError as ex:
|
||||
raise FileNotFoundError('Patch file missing from archive. Invalid Player ID.')
|
||||
else:
|
||||
with open(file, 'rb') as stream:
|
||||
patch_data = stream.read()
|
||||
patch_data = BigStream(zlib.decompress(patch_data))
|
||||
|
||||
# make sure the header is correct
|
||||
if patch_data.read_bytes(length=4) != b'ZPFv':
|
||||
raise Exception("File is not in a Zelda Patch Format")
|
||||
if patch_data.read_byte() != ord('1'):
|
||||
# in the future we might want to have revisions for this format
|
||||
raise Exception("Unsupported patch version.")
|
||||
|
||||
# load the patch configuration info. The fact that the DMA Table is
|
||||
# included in the patch is so that this might be able to work with
|
||||
# other N64 games.
|
||||
dma_start = patch_data.read_int32()
|
||||
xor_range = (patch_data.read_int32(), patch_data.read_int32())
|
||||
xor_address = patch_data.read_int32()
|
||||
|
||||
# Load all the DMA table updates. This will move the files around.
|
||||
# A key thing is that some of these entries will list a source file
|
||||
# that they are from, so we know where to copy from, no matter where
|
||||
# in the DMA table this file has been moved to. Also important if a file
|
||||
# is copied. This list is terminated with 0xFFFF
|
||||
while True:
|
||||
# Load DMA update
|
||||
dma_index = patch_data.read_int16()
|
||||
if dma_index == 0xFFFF:
|
||||
break
|
||||
|
||||
from_file = patch_data.read_int32()
|
||||
start = patch_data.read_int32()
|
||||
size = patch_data.read_int24()
|
||||
|
||||
# Save new DMA Table entry
|
||||
dma_entry = dma_start + (dma_index * 0x10)
|
||||
end = start + size
|
||||
rom.write_int32(dma_entry, start)
|
||||
rom.write_int32(None, end)
|
||||
rom.write_int32(None, start)
|
||||
rom.write_int32(None, 0)
|
||||
|
||||
if from_file != 0xFFFFFFFF:
|
||||
# If a source file is listed, copy from there
|
||||
old_dma_start, old_dma_end, old_size = rom.original.get_dmadata_record_by_key(from_file)
|
||||
copy_size = min(size, old_size)
|
||||
rom.write_bytes(start, rom.original.read_bytes(from_file, copy_size))
|
||||
rom.buffer[start+copy_size:start+size] = [0] * (size - copy_size)
|
||||
else:
|
||||
# if it's a new file, fill with 0s
|
||||
rom.buffer[start:start+size] = [0] * size
|
||||
|
||||
# Read in the XOR data blocks. This goes to the end of the file.
|
||||
block_start = None
|
||||
while not patch_data.eof():
|
||||
is_new_block = patch_data.read_byte() != 0xFF
|
||||
|
||||
if is_new_block:
|
||||
# start writing a new block
|
||||
patch_data.seek_address(delta=-1)
|
||||
block_start = patch_data.read_int32()
|
||||
block_size = patch_data.read_int16()
|
||||
else:
|
||||
# continue writing from previous block
|
||||
key_skip = patch_data.read_byte()
|
||||
block_size = patch_data.read_int16()
|
||||
# skip specified XOR keys
|
||||
for _ in range(key_skip):
|
||||
key, xor_address = key_next(rom, xor_address, xor_range)
|
||||
|
||||
# read in the new data
|
||||
data = []
|
||||
for b in patch_data.read_bytes(length=block_size):
|
||||
if b == 0:
|
||||
# keep 0s as 0s
|
||||
data += [0]
|
||||
else:
|
||||
# The XOR will always be safe and will never produce 0
|
||||
key, xor_address = key_next(rom, xor_address, xor_range)
|
||||
data += [b ^ key]
|
||||
|
||||
# Save the new data to rom
|
||||
rom.write_bytes(block_start, data)
|
||||
block_start = block_start+block_size
|
||||
|
||||
782
worlds/oot/Options.py
Normal file
782
worlds/oot/Options.py
Normal file
@@ -0,0 +1,782 @@
|
||||
import typing
|
||||
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionList
|
||||
from .Colors import *
|
||||
import worlds.oot.Sounds as sfx
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
"""Set the logic used for the generator."""
|
||||
displayname = "Logic Rules"
|
||||
option_glitchless = 0
|
||||
option_glitched = 1
|
||||
option_no_logic = 2
|
||||
|
||||
|
||||
class NightTokens(Toggle):
|
||||
"""Nighttime skulltulas will logically require Sun's Song."""
|
||||
displayname = "Nighttime Skulltulas Expect Sun's Song"
|
||||
|
||||
|
||||
class Forest(Choice):
|
||||
"""Set the state of Kokiri Forest and the path to Deku Tree."""
|
||||
displayname = "Forest"
|
||||
option_open = 0
|
||||
option_closed_deku = 1
|
||||
option_closed = 2
|
||||
alias_open_forest = 0
|
||||
alias_closed_forest = 2
|
||||
|
||||
|
||||
class Gate(Choice):
|
||||
"""Set the state of the Kakariko Village gate."""
|
||||
displayname = "Kakariko Gate"
|
||||
option_open = 0
|
||||
option_zelda = 1
|
||||
option_closed = 2
|
||||
|
||||
|
||||
class DoorOfTime(DefaultOnToggle):
|
||||
"""Open the Door of Time by default, without the Song of Time."""
|
||||
displayname = "Open Door of Time"
|
||||
|
||||
|
||||
class Fountain(Choice):
|
||||
"""Set the state of King Zora, blocking the way to Zora's Fountain."""
|
||||
displayname = "Zora's Fountain"
|
||||
option_open = 0
|
||||
option_adult = 1
|
||||
option_closed = 2
|
||||
default = 2
|
||||
|
||||
|
||||
class Fortress(Choice):
|
||||
"""Set the requirements for access to Gerudo Fortress."""
|
||||
displayname = "Gerudo Fortress"
|
||||
option_normal = 0
|
||||
option_fast = 1
|
||||
option_open = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class Bridge(Choice):
|
||||
"""Set the requirements for the Rainbow Bridge."""
|
||||
displayname = "Rainbow Bridge Requirement"
|
||||
option_open = 0
|
||||
option_vanilla = 1
|
||||
option_stones = 2
|
||||
option_medallions = 3
|
||||
option_dungeons = 4
|
||||
option_tokens = 5
|
||||
default = 3
|
||||
|
||||
|
||||
class Trials(Range):
|
||||
"""Set the number of required trials in Ganon's Castle."""
|
||||
displayname = "Ganon's Trials Count"
|
||||
range_start = 0
|
||||
range_end = 6
|
||||
|
||||
|
||||
open_options: typing.Dict[str, type(Option)] = {
|
||||
"open_forest": Forest,
|
||||
"open_kakariko": Gate,
|
||||
"open_door_of_time": DoorOfTime,
|
||||
"zora_fountain": Fountain,
|
||||
"gerudo_fortress": Fortress,
|
||||
"bridge": Bridge,
|
||||
"trials": Trials,
|
||||
}
|
||||
|
||||
|
||||
class StartingAge(Choice):
|
||||
"""Choose which age Link will start as."""
|
||||
displayname = "Starting Age"
|
||||
option_child = 0
|
||||
option_adult = 1
|
||||
|
||||
|
||||
# TODO: document and name ER options
|
||||
class InteriorEntrances(Choice):
|
||||
option_off = 0
|
||||
option_simple = 1
|
||||
option_all = 2
|
||||
alias_false = 0
|
||||
|
||||
|
||||
class TriforceHunt(Toggle):
|
||||
"""Gather pieces of the Triforce scattered around the world to complete the game."""
|
||||
displayname = "Triforce Hunt"
|
||||
|
||||
|
||||
class TriforceGoal(Range):
|
||||
"""Number of Triforce pieces required to complete the game. Total number placed determined by the Item Pool setting."""
|
||||
displayname = "Required Triforce Pieces"
|
||||
range_start = 1
|
||||
range_end = 50
|
||||
default = 20
|
||||
|
||||
|
||||
class LogicalChus(Toggle):
|
||||
"""Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell refills; bombchus open Bombchu Bowling."""
|
||||
displayname = "Bombchus Considered in Logic"
|
||||
|
||||
|
||||
world_options: typing.Dict[str, type(Option)] = {
|
||||
"starting_age": StartingAge,
|
||||
# "shuffle_interior_entrances": InteriorEntrances,
|
||||
# "shuffle_grotto_entrances": Toggle,
|
||||
# "shuffle_dungeon_entrances": Toggle,
|
||||
# "shuffle_overworld_entrances": Toggle,
|
||||
# "owl_drops": Toggle,
|
||||
# "warp_songs": Toggle,
|
||||
# "spawn_positions": Toggle,
|
||||
"triforce_hunt": TriforceHunt,
|
||||
"triforce_goal": TriforceGoal,
|
||||
"bombchus_in_logic": LogicalChus,
|
||||
# "mq_dungeons": make_range(0, 12),
|
||||
}
|
||||
|
||||
|
||||
class LacsCondition(Choice):
|
||||
"""Set the requirements for the Light Arrow Cutscene in the Temple of Time."""
|
||||
displayname = "Light Arrow Cutscene Requirement"
|
||||
option_vanilla = 0
|
||||
option_stones = 1
|
||||
option_medallions = 2
|
||||
option_dungeons = 3
|
||||
option_tokens = 4
|
||||
|
||||
|
||||
class LacsStones(Range):
|
||||
"""Set the number of Spiritual Stones required for LACS."""
|
||||
displayname = "Spiritual Stones Required for LACS"
|
||||
range_start = 0
|
||||
range_end = 3
|
||||
default = 3
|
||||
|
||||
|
||||
class LacsMedallions(Range):
|
||||
"""Set the number of medallions required for LACS."""
|
||||
displayname = "Medallions Required for LACS"
|
||||
range_start = 0
|
||||
range_end = 6
|
||||
default = 6
|
||||
|
||||
|
||||
class LacsRewards(Range):
|
||||
"""Set the number of dungeon rewards required for LACS."""
|
||||
displayname = "Dungeon Rewards Required for LACS"
|
||||
range_start = 0
|
||||
range_end = 9
|
||||
default = 9
|
||||
|
||||
|
||||
class LacsTokens(Range):
|
||||
"""Set the number of Gold Skulltula Tokens required for LACS."""
|
||||
displayname = "Tokens Required for LACS"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 100
|
||||
|
||||
|
||||
lacs_options: typing.Dict[str, type(Option)] = {
|
||||
"lacs_condition": LacsCondition,
|
||||
"lacs_stones": LacsStones,
|
||||
"lacs_medallions": LacsMedallions,
|
||||
"lacs_rewards": LacsRewards,
|
||||
"lacs_tokens": LacsTokens,
|
||||
}
|
||||
|
||||
|
||||
class BridgeStones(Range):
|
||||
"""Set the number of Spiritual Stones required for the rainbow bridge."""
|
||||
displayname = "Spiritual Stones Required for Bridge"
|
||||
range_start = 0
|
||||
range_end = 3
|
||||
default = 3
|
||||
|
||||
|
||||
class BridgeMedallions(Range):
|
||||
"""Set the number of medallions required for the rainbow bridge."""
|
||||
displayname = "Medallions Required for Bridge"
|
||||
range_start = 0
|
||||
range_end = 6
|
||||
default = 6
|
||||
|
||||
|
||||
class BridgeRewards(Range):
|
||||
"""Set the number of dungeon rewards required for the rainbow bridge."""
|
||||
displayname = "Dungeon Rewards Required for Bridge"
|
||||
range_start = 0
|
||||
range_end = 9
|
||||
default = 9
|
||||
|
||||
|
||||
class BridgeTokens(Range):
|
||||
"""Set the number of Gold Skulltula Tokens required for the rainbow bridge."""
|
||||
displayname = "Tokens Required for Bridge"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 100
|
||||
|
||||
|
||||
bridge_options: typing.Dict[str, type(Option)] = {
|
||||
"bridge_stones": BridgeStones,
|
||||
"bridge_medallions": BridgeMedallions,
|
||||
"bridge_rewards": BridgeRewards,
|
||||
"bridge_tokens": BridgeTokens,
|
||||
}
|
||||
|
||||
|
||||
class SongShuffle(Choice):
|
||||
"""Set where songs can appear."""
|
||||
displayname = "Shuffle Songs"
|
||||
option_song = 0
|
||||
option_dungeon = 1
|
||||
option_any = 2
|
||||
|
||||
|
||||
class ShopShuffle(Choice):
|
||||
"""Randomizes shop contents. Set to "off" to not shuffle shops; "0" shuffles shops but does not allow multiworld items in shops."""
|
||||
displayname = "Shopsanity"
|
||||
option_0 = 0
|
||||
option_1 = 1
|
||||
option_2 = 2
|
||||
option_3 = 3
|
||||
option_4 = 4
|
||||
option_random_value = 5
|
||||
option_off = 6
|
||||
default = 6
|
||||
alias_false = 6
|
||||
|
||||
|
||||
class TokenShuffle(Choice):
|
||||
"""Token rewards from Gold Skulltulas are shuffled into the pool."""
|
||||
displayname = "Tokensanity"
|
||||
option_off = 0
|
||||
option_dungeons = 1
|
||||
option_overworld = 2
|
||||
option_all = 3
|
||||
alias_false = 0
|
||||
|
||||
|
||||
class ScrubShuffle(Choice):
|
||||
"""Shuffle the items sold by Business Scrubs, and set the prices."""
|
||||
displayname = "Scrub Shuffle"
|
||||
option_off = 0
|
||||
option_low = 1
|
||||
option_regular = 2
|
||||
option_random_prices = 3
|
||||
alias_false = 0
|
||||
alias_affordable = 1
|
||||
alias_expensive = 2
|
||||
|
||||
|
||||
class ShuffleCows(Toggle):
|
||||
"""Cows give items when Epona's Song is played."""
|
||||
displayname = "Shuffle Cows"
|
||||
|
||||
|
||||
class ShuffleSword(Toggle):
|
||||
"""Shuffle Kokiri Sword into the item pool."""
|
||||
displayname = "Shuffle Kokiri Sword"
|
||||
|
||||
|
||||
class ShuffleOcarinas(Toggle):
|
||||
"""Shuffle the Fairy Ocarina and Ocarina of Time into the item pool."""
|
||||
displayname = "Shuffle Ocarinas"
|
||||
|
||||
|
||||
class ShuffleEgg(Toggle):
|
||||
"""Shuffle the Weird Egg from Malon at Hyrule Castle."""
|
||||
displayname = "Shuffle Weird Egg"
|
||||
|
||||
|
||||
class ShuffleCard(Toggle):
|
||||
"""Shuffle the Gerudo Membership Card into the item pool."""
|
||||
displayname = "Shuffle Gerudo Card"
|
||||
|
||||
|
||||
class ShuffleBeans(Toggle):
|
||||
"""Adds a pack of 10 beans to the item pool and changes the bean salesman to sell one item for 60 rupees."""
|
||||
displayname = "Shuffle Magic Beans"
|
||||
|
||||
|
||||
class ShuffleMedigoronCarpet(Toggle):
|
||||
"""Shuffle the items sold by Medigoron and the Haunted Wasteland Carpet Salesman."""
|
||||
displayname = "Shuffle Medigoron & Carpet Salesman"
|
||||
|
||||
|
||||
shuffle_options: typing.Dict[str, type(Option)] = {
|
||||
"shuffle_song_items": SongShuffle,
|
||||
"shopsanity": ShopShuffle,
|
||||
"tokensanity": TokenShuffle,
|
||||
"shuffle_scrubs": ScrubShuffle,
|
||||
"shuffle_cows": ShuffleCows,
|
||||
"shuffle_kokiri_sword": ShuffleSword,
|
||||
"shuffle_ocarinas": ShuffleOcarinas,
|
||||
"shuffle_weird_egg": ShuffleEgg,
|
||||
"shuffle_gerudo_card": ShuffleCard,
|
||||
"shuffle_beans": ShuffleBeans,
|
||||
"shuffle_medigoron_carpet_salesman": ShuffleMedigoronCarpet,
|
||||
}
|
||||
|
||||
|
||||
class ShuffleMapCompass(Choice):
|
||||
"""Control where to shuffle dungeon maps and compasses."""
|
||||
displayname = "Maps & Compasses"
|
||||
option_remove = 0
|
||||
option_startwith = 1
|
||||
option_vanilla = 2
|
||||
option_dungeon = 3
|
||||
option_overworld = 4
|
||||
option_any_dungeon = 5
|
||||
option_keysanity = 6
|
||||
default = 1
|
||||
alias_anywhere = 6
|
||||
|
||||
|
||||
class ShuffleKeys(Choice):
|
||||
"""Control where to shuffle dungeon small keys."""
|
||||
displayname = "Small Keys"
|
||||
option_remove = 0
|
||||
option_vanilla = 2
|
||||
option_dungeon = 3
|
||||
option_overworld = 4
|
||||
option_any_dungeon = 5
|
||||
option_keysanity = 6
|
||||
default = 3
|
||||
alias_keysy = 0
|
||||
alias_anywhere = 6
|
||||
|
||||
|
||||
class ShuffleGerudoKeys(Choice):
|
||||
"""Control where to shuffle the Gerudo Fortress small keys."""
|
||||
displayname = "Gerudo Fortress Keys"
|
||||
option_vanilla = 0
|
||||
option_overworld = 1
|
||||
option_any_dungeon = 2
|
||||
option_keysanity = 3
|
||||
alias_anywhere = 3
|
||||
|
||||
|
||||
class ShuffleBossKeys(Choice):
|
||||
"""Control where to shuffle boss keys, except the Ganon's Castle Boss Key."""
|
||||
displayname = "Boss Keys"
|
||||
option_remove = 0
|
||||
option_vanilla = 2
|
||||
option_dungeon = 3
|
||||
option_overworld = 4
|
||||
option_any_dungeon = 5
|
||||
option_keysanity = 6
|
||||
default = 3
|
||||
alias_keysy = 0
|
||||
alias_anywhere = 6
|
||||
|
||||
|
||||
class ShuffleGanonBK(Choice):
|
||||
"""Control where to shuffle the Ganon's Castle Boss Key."""
|
||||
displayname = "Ganon's Boss Key"
|
||||
option_remove = 0
|
||||
option_vanilla = 2
|
||||
option_dungeon = 3
|
||||
option_overworld = 4
|
||||
option_any_dungeon = 5
|
||||
option_keysanity = 6
|
||||
option_on_lacs = 7
|
||||
default = 0
|
||||
alias_keysy = 0
|
||||
alias_anywhere = 6
|
||||
|
||||
|
||||
class EnhanceMC(Toggle):
|
||||
"""Map tells if a dungeon is vanilla or MQ. Compass tells what the dungeon reward is."""
|
||||
displayname = "Maps and Compasses Give Information"
|
||||
|
||||
|
||||
dungeon_items_options: typing.Dict[str, type(Option)] = {
|
||||
"shuffle_mapcompass": ShuffleMapCompass,
|
||||
"shuffle_smallkeys": ShuffleKeys,
|
||||
"shuffle_fortresskeys": ShuffleGerudoKeys,
|
||||
"shuffle_bosskeys": ShuffleBossKeys,
|
||||
"shuffle_ganon_bosskey": ShuffleGanonBK,
|
||||
"enhance_map_compass": EnhanceMC,
|
||||
}
|
||||
|
||||
|
||||
class SkipChildZelda(Toggle):
|
||||
"""Game starts with Zelda's Letter, the item at Zelda's Lullaby, and the relevant events already completed."""
|
||||
displayname = "Skip Child Zelda"
|
||||
|
||||
|
||||
class SkipEscape(DefaultOnToggle):
|
||||
"""Skips the tower collapse sequence between the Ganondorf and Ganon fights."""
|
||||
displayname = "Skip Tower Escape Sequence"
|
||||
|
||||
|
||||
class SkipStealth(DefaultOnToggle):
|
||||
"""The crawlspace into Hyrule Castle skips straight to Zelda."""
|
||||
displayname = "Skip Child Stealth"
|
||||
|
||||
|
||||
class SkipEponaRace(DefaultOnToggle):
|
||||
"""Epona can always be summoned with Epona's Song."""
|
||||
displayname = "Skip Epona Race"
|
||||
|
||||
|
||||
class SkipMinigamePhases(DefaultOnToggle):
|
||||
"""Dampe Race and Horseback Archery give both rewards if the second condition is met on the first attempt."""
|
||||
displayname = "Skip Some Minigame Phases"
|
||||
|
||||
|
||||
class CompleteMaskQuest(Toggle):
|
||||
"""All masks are immediately available to borrow from the Happy Mask Shop."""
|
||||
displayname = "Complete Mask Quest"
|
||||
|
||||
|
||||
class UsefulCutscenes(Toggle):
|
||||
"""Reenables the Poe cutscene in Forest Temple, Darunia in Fire Temple, and Twinrova introduction. Mostly useful for glitched."""
|
||||
displayname = "Enable Useful Cutscenes"
|
||||
|
||||
|
||||
class FastChests(DefaultOnToggle):
|
||||
"""All chest animations are fast. If disabled, major items have a slow animation."""
|
||||
displayname = "Fast Chest Cutscenes"
|
||||
|
||||
|
||||
class FreeScarecrow(Toggle):
|
||||
"""Pulling out the ocarina near a scarecrow spot spawns Pierre without needing the song."""
|
||||
displayname = "Free Scarecrow's Song"
|
||||
|
||||
|
||||
class FastBunny(Toggle):
|
||||
"""Bunny Hood lets you move 1.5x faster like in Majora's Mask."""
|
||||
displayname = "Fast Bunny Hood"
|
||||
|
||||
|
||||
class ChickenCount(Range):
|
||||
"""Controls the number of Cuccos for Anju to give an item as child."""
|
||||
displayname = "Cucco Count"
|
||||
range_start = 0
|
||||
range_end = 7
|
||||
default = 7
|
||||
|
||||
|
||||
timesavers_options: typing.Dict[str, type(Option)] = {
|
||||
"skip_child_zelda": SkipChildZelda,
|
||||
"no_escape_sequence": SkipEscape,
|
||||
"no_guard_stealth": SkipStealth,
|
||||
"no_epona_race": SkipEponaRace,
|
||||
"skip_some_minigame_phases": SkipMinigamePhases,
|
||||
"complete_mask_quest": CompleteMaskQuest,
|
||||
"useful_cutscenes": UsefulCutscenes,
|
||||
"fast_chests": FastChests,
|
||||
"free_scarecrow": FreeScarecrow,
|
||||
"fast_bunny_hood": FastBunny,
|
||||
"chicken_count": ChickenCount,
|
||||
# "big_poe_count": make_range(1, 10, 1),
|
||||
}
|
||||
|
||||
|
||||
class Hints(Choice):
|
||||
"""Gossip Stones can give hints about item locations."""
|
||||
displayname = "Gossip Stones"
|
||||
option_none = 0
|
||||
option_mask = 1
|
||||
option_agony = 2
|
||||
option_always = 3
|
||||
default = 3
|
||||
alias_false = 0
|
||||
|
||||
|
||||
class HintDistribution(Choice):
|
||||
"""Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc."""
|
||||
displayname = "Hint Distribution"
|
||||
option_balanced = 0
|
||||
option_ddr = 1
|
||||
option_league = 2
|
||||
option_mw2 = 3
|
||||
option_scrubs = 4
|
||||
option_strong = 5
|
||||
option_tournament = 6
|
||||
option_useless = 7
|
||||
option_very_strong = 8
|
||||
|
||||
|
||||
class TextShuffle(Choice):
|
||||
"""Randomizes text in the game for comedic effect."""
|
||||
displayname = "Text Shuffle"
|
||||
option_none = 0
|
||||
option_except_hints = 1
|
||||
option_complete = 2
|
||||
alias_false = 0
|
||||
|
||||
|
||||
class DamageMultiplier(Choice):
|
||||
"""Controls the amount of damage Link takes."""
|
||||
displayname = "Damage Multiplier"
|
||||
option_half = 0
|
||||
option_normal = 1
|
||||
option_double = 2
|
||||
option_quadruple = 3
|
||||
option_ohko = 4
|
||||
default = 1
|
||||
|
||||
|
||||
class HeroMode(Toggle):
|
||||
"""Hearts will not drop from enemies or objects."""
|
||||
displayname = "Hero Mode"
|
||||
|
||||
|
||||
class StartingToD(Choice):
|
||||
"""Change the starting time of day."""
|
||||
displayname = "Starting Time of Day"
|
||||
option_default = 0
|
||||
option_sunrise = 1
|
||||
option_morning = 2
|
||||
option_noon = 3
|
||||
option_afternoon = 4
|
||||
option_sunset = 5
|
||||
option_evening = 6
|
||||
option_midnight = 7
|
||||
option_witching_hour = 8
|
||||
|
||||
|
||||
class ConsumableStart(Toggle):
|
||||
"""Start the game with full Deku Sticks and Deku Nuts."""
|
||||
displayname = "Start with Consumables"
|
||||
|
||||
|
||||
class RupeeStart(Toggle):
|
||||
"""Start with a full wallet. Wallet upgrades will also fill your wallet."""
|
||||
displayname = "Start with Rupees"
|
||||
|
||||
|
||||
misc_options: typing.Dict[str, type(Option)] = {
|
||||
# "clearer_hints": DefaultOnToggle,
|
||||
"hints": Hints,
|
||||
"hint_dist": HintDistribution,
|
||||
"text_shuffle": TextShuffle,
|
||||
"damage_multiplier": DamageMultiplier,
|
||||
"no_collectible_hearts": HeroMode,
|
||||
"starting_tod": StartingToD,
|
||||
"start_with_consumables": ConsumableStart,
|
||||
"start_with_rupees": RupeeStart,
|
||||
}
|
||||
|
||||
class ItemPoolValue(Choice):
|
||||
"""Changes the number of items available in the game."""
|
||||
displayname = "Item Pool"
|
||||
option_plentiful = 0
|
||||
option_balanced = 1
|
||||
option_scarce = 2
|
||||
option_minimal = 3
|
||||
default = 1
|
||||
|
||||
|
||||
class IceTraps(Choice):
|
||||
"""Adds ice traps to the item pool."""
|
||||
displayname = "Ice Traps"
|
||||
option_off = 0
|
||||
option_normal = 1
|
||||
option_on = 2
|
||||
option_mayhem = 3
|
||||
option_onslaught = 4
|
||||
default = 1
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
alias_extra = 2
|
||||
|
||||
|
||||
class IceTrapVisual(Choice):
|
||||
"""Changes the appearance of ice traps as freestanding items."""
|
||||
displayname = "Ice Trap Appearance"
|
||||
option_major_only = 0
|
||||
option_junk_only = 1
|
||||
option_anything = 2
|
||||
|
||||
|
||||
class AdultTradeItem(Choice):
|
||||
option_pocket_egg = 0
|
||||
option_pocket_cucco = 1
|
||||
option_cojiro = 2
|
||||
option_odd_mushroom = 3
|
||||
option_poachers_saw = 4
|
||||
option_broken_sword = 5
|
||||
option_prescription = 6
|
||||
option_eyeball_frog = 7
|
||||
option_eyedrops = 8
|
||||
option_claim_check = 9
|
||||
|
||||
|
||||
class EarlyTradeItem(AdultTradeItem):
|
||||
"""Earliest item that can appear in the adult trade sequence."""
|
||||
displayname = "Adult Trade Sequence Earliest Item"
|
||||
default = 6
|
||||
|
||||
|
||||
class LateTradeItem(AdultTradeItem):
|
||||
"""Latest item that can appear in the adult trade sequence."""
|
||||
displayname = "Adult Trade Sequence Latest Item"
|
||||
default = 9
|
||||
|
||||
|
||||
itempool_options: typing.Dict[str, type(Option)] = {
|
||||
"item_pool_value": ItemPoolValue,
|
||||
"junk_ice_traps": IceTraps,
|
||||
"ice_trap_appearance": IceTrapVisual,
|
||||
"logic_earliest_adult_trade": EarlyTradeItem,
|
||||
"logic_latest_adult_trade": LateTradeItem,
|
||||
}
|
||||
|
||||
# Start of cosmetic options
|
||||
|
||||
def assemble_color_option(func, display_name: str, default_option: str, outer=False):
|
||||
color_options = func()
|
||||
if outer:
|
||||
color_options.append("Match Inner")
|
||||
format_color = lambda color: color.replace(' ', '_').lower()
|
||||
color_to_id = {format_color(color): index for index, color in enumerate(color_options)}
|
||||
class ColorOption(Choice):
|
||||
"""Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
|
||||
displayname = display_name
|
||||
default = color_options.index(default_option)
|
||||
ColorOption.options.update(color_to_id)
|
||||
ColorOption.name_lookup.update({id: color for (color, id) in color_to_id.items()})
|
||||
return ColorOption
|
||||
|
||||
|
||||
class Targeting(Choice):
|
||||
"""Default targeting option."""
|
||||
displayname = "Default Targeting Option"
|
||||
option_hold = 0
|
||||
option_switch = 1
|
||||
|
||||
|
||||
class DisplayDpad(DefaultOnToggle):
|
||||
"""Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots)."""
|
||||
displayname = "Display D-Pad HUD"
|
||||
|
||||
|
||||
class CorrectColors(DefaultOnToggle):
|
||||
"""Makes in-game models match their HUD element colors."""
|
||||
displayname = "Item Model Colors Match Cosmetics"
|
||||
|
||||
|
||||
class Music(Choice):
|
||||
option_normal = 0
|
||||
option_off = 1
|
||||
option_randomized = 2
|
||||
alias_false = 1
|
||||
|
||||
|
||||
class BackgroundMusic(Music):
|
||||
"""Randomize or disable background music."""
|
||||
displayname = "Background Music"
|
||||
|
||||
|
||||
class Fanfares(Music):
|
||||
"""Randomize or disable item fanfares."""
|
||||
displayname = "Fanfares"
|
||||
|
||||
|
||||
class OcarinaFanfares(Toggle):
|
||||
"""Enable ocarina songs as fanfares. These are longer than usual fanfares. Does nothing without fanfares randomized."""
|
||||
displayname = "Ocarina Songs as Fanfares"
|
||||
|
||||
|
||||
class SwordTrailDuration(Range):
|
||||
"""Set the duration for sword trails."""
|
||||
displayname = "Sword Trail Duration"
|
||||
range_start = 4
|
||||
range_end = 20
|
||||
default = 4
|
||||
|
||||
|
||||
cosmetic_options: typing.Dict[str, type(Option)] = {
|
||||
"default_targeting": Targeting,
|
||||
"display_dpad": DisplayDpad,
|
||||
"correct_model_colors": CorrectColors,
|
||||
"background_music": BackgroundMusic,
|
||||
"fanfares": Fanfares,
|
||||
"ocarina_fanfares": OcarinaFanfares,
|
||||
"kokiri_color": assemble_color_option(get_tunic_color_options, "Kokiri Tunic", "Kokiri Green"),
|
||||
"goron_color": assemble_color_option(get_tunic_color_options, "Goron Tunic", "Goron Red"),
|
||||
"zora_color": assemble_color_option(get_tunic_color_options, "Zora Tunic", "Zora Blue"),
|
||||
"silver_gauntlets_color": assemble_color_option(get_gauntlet_color_options, "Silver Gauntlets Color", "Silver"),
|
||||
"golden_gauntlets_color": assemble_color_option(get_gauntlet_color_options, "Golden Gauntlets Color", "Gold"),
|
||||
"mirror_shield_frame_color": assemble_color_option(get_shield_frame_color_options, "Mirror Shield Frame Color", "Red"),
|
||||
"navi_color_default_inner": assemble_color_option(get_navi_color_options, "Navi Idle Inner", "White"),
|
||||
"navi_color_default_outer": assemble_color_option(get_navi_color_options, "Navi Idle Outer", "Match Inner", outer=True),
|
||||
"navi_color_enemy_inner": assemble_color_option(get_navi_color_options, "Navi Targeting Enemy Inner", "Yellow"),
|
||||
"navi_color_enemy_outer": assemble_color_option(get_navi_color_options, "Navi Targeting Enemy Outer", "Match Inner", outer=True),
|
||||
"navi_color_npc_inner": assemble_color_option(get_navi_color_options, "Navi Targeting NPC Inner", "Light Blue"),
|
||||
"navi_color_npc_outer": assemble_color_option(get_navi_color_options, "Navi Targeting NPC Outer", "Match Inner", outer=True),
|
||||
"navi_color_prop_inner": assemble_color_option(get_navi_color_options, "Navi Targeting Prop Inner", "Green"),
|
||||
"navi_color_prop_outer": assemble_color_option(get_navi_color_options, "Navi Targeting Prop Outer", "Match Inner", outer=True),
|
||||
"sword_trail_duration": SwordTrailDuration,
|
||||
"sword_trail_color_inner": assemble_color_option(get_sword_trail_color_options, "Sword Trail Inner", "White"),
|
||||
"sword_trail_color_outer": assemble_color_option(get_sword_trail_color_options, "Sword Trail Outer", "Match Inner", outer=True),
|
||||
"bombchu_trail_color_inner": assemble_color_option(get_bombchu_trail_color_options, "Bombchu Trail Inner", "Red"),
|
||||
"bombchu_trail_color_outer": assemble_color_option(get_bombchu_trail_color_options, "Bombchu Trail Outer", "Match Inner", outer=True),
|
||||
"boomerang_trail_color_inner": assemble_color_option(get_boomerang_trail_color_options, "Boomerang Trail Inner", "Yellow"),
|
||||
"boomerang_trail_color_outer": assemble_color_option(get_boomerang_trail_color_options, "Boomerang Trail Outer", "Match Inner", outer=True),
|
||||
"heart_color": assemble_color_option(get_heart_color_options, "Heart Color", "Red"),
|
||||
"magic_color": assemble_color_option(get_magic_color_options, "Magic Color", "Green"),
|
||||
"a_button_color": assemble_color_option(get_a_button_color_options, "A Button Color", "N64 Blue"),
|
||||
"b_button_color": assemble_color_option(get_b_button_color_options, "B Button Color", "N64 Green"),
|
||||
"c_button_color": assemble_color_option(get_c_button_color_options, "C Button Color", "Yellow"),
|
||||
"start_button_color": assemble_color_option(get_start_button_color_options, "Start Button Color", "N64 Red"),
|
||||
}
|
||||
|
||||
def assemble_sfx_option(sound_hook: sfx.SoundHooks, display_name: str):
|
||||
options = sfx.get_setting_choices(sound_hook).keys()
|
||||
sfx_to_id = {sfx.replace('-', '_'): index for index, sfx in enumerate(options)}
|
||||
class SfxOption(Choice):
|
||||
"""Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound."""
|
||||
displayname = display_name
|
||||
SfxOption.options.update(sfx_to_id)
|
||||
SfxOption.name_lookup.update({id: sfx for (sfx, id) in sfx_to_id.items()})
|
||||
return SfxOption
|
||||
|
||||
class SfxOcarina(Choice):
|
||||
"""Change the sound of the ocarina."""
|
||||
displayname = "Ocarina Instrument"
|
||||
option_ocarina = 1
|
||||
option_malon = 2
|
||||
option_whistle = 3
|
||||
option_harp = 4
|
||||
option_grind_organ = 5
|
||||
option_flute = 6
|
||||
default = 1
|
||||
|
||||
sfx_options: typing.Dict[str, type(Option)] = {
|
||||
"sfx_navi_overworld": assemble_sfx_option(sfx.SoundHooks.NAVI_OVERWORLD, "Navi Overworld"),
|
||||
"sfx_navi_enemy": assemble_sfx_option(sfx.SoundHooks.NAVI_ENEMY, "Navi Enemy"),
|
||||
"sfx_low_hp": assemble_sfx_option(sfx.SoundHooks.HP_LOW, "Low HP"),
|
||||
"sfx_menu_cursor": assemble_sfx_option(sfx.SoundHooks.MENU_CURSOR, "Menu Cursor"),
|
||||
"sfx_menu_select": assemble_sfx_option(sfx.SoundHooks.MENU_SELECT, "Menu Select"),
|
||||
"sfx_nightfall": assemble_sfx_option(sfx.SoundHooks.NIGHTFALL, "Nightfall"),
|
||||
"sfx_horse_neigh": assemble_sfx_option(sfx.SoundHooks.HORSE_NEIGH, "Horse"),
|
||||
"sfx_hover_boots": assemble_sfx_option(sfx.SoundHooks.BOOTS_HOVER, "Hover Boots"),
|
||||
"sfx_ocarina": SfxOcarina,
|
||||
}
|
||||
|
||||
|
||||
# All options assembled into a single dict
|
||||
oot_options: typing.Dict[str, type(Option)] = {
|
||||
"logic_rules": Logic,
|
||||
"logic_no_night_tokens_without_suns_song": NightTokens,
|
||||
**open_options,
|
||||
**world_options,
|
||||
**bridge_options,
|
||||
**dungeon_items_options,
|
||||
**lacs_options,
|
||||
**shuffle_options,
|
||||
**timesavers_options,
|
||||
**misc_options,
|
||||
**itempool_options,
|
||||
**cosmetic_options,
|
||||
**sfx_options,
|
||||
"logic_tricks": OptionList,
|
||||
}
|
||||
2166
worlds/oot/Patches.py
Normal file
2166
worlds/oot/Patches.py
Normal file
File diff suppressed because it is too large
Load Diff
61
worlds/oot/Regions.py
Normal file
61
worlds/oot/Regions.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from enum import unique, Enum
|
||||
|
||||
from BaseClasses import Region
|
||||
|
||||
|
||||
# copied from OoT-Randomizer/Region.py
|
||||
@unique
|
||||
class RegionType(Enum):
|
||||
|
||||
Overworld = 1
|
||||
Interior = 2
|
||||
Dungeon = 3
|
||||
Grotto = 4
|
||||
|
||||
|
||||
@property
|
||||
def is_indoors(self):
|
||||
"""Shorthand for checking if Interior or Dungeon"""
|
||||
return self in (RegionType.Interior, RegionType.Dungeon, RegionType.Grotto)
|
||||
|
||||
# Pretends to be an enum, but when the values are raw ints, it's much faster
|
||||
class TimeOfDay(object):
|
||||
NONE = 0
|
||||
DAY = 1
|
||||
DAMPE = 2
|
||||
ALL = DAY | DAMPE
|
||||
|
||||
|
||||
|
||||
|
||||
class OOTRegion(Region):
|
||||
game: str = "Ocarina of Time"
|
||||
|
||||
def __init__(self, name: str, type, hint, player: int):
|
||||
super(OOTRegion, self).__init__(name, type, hint, player)
|
||||
self.price = None
|
||||
self.time_passes = False
|
||||
self.provides_time = TimeOfDay.NONE
|
||||
self.scene = None
|
||||
self.dungeon = None
|
||||
|
||||
def get_scene(self):
|
||||
if self.scene:
|
||||
return self.scene
|
||||
elif self.dungeon:
|
||||
return self.dungeon.name
|
||||
else:
|
||||
return None
|
||||
|
||||
def can_reach(self, state):
|
||||
if state.stale[self.player]:
|
||||
stored_age = state.age[self.player]
|
||||
state._oot_update_age_reachable_regions(self.player)
|
||||
state.age[self.player] = stored_age
|
||||
if state.age[self.player] == 'child':
|
||||
return self in state.child_reachable_regions[self.player]
|
||||
elif state.age[self.player] == 'adult':
|
||||
return self in state.adult_reachable_regions[self.player]
|
||||
else: # we don't care about age
|
||||
return self in state.child_reachable_regions[self.player] or self in state.adult_reachable_regions[self.player]
|
||||
|
||||
300
worlds/oot/Rom.py
Normal file
300
worlds/oot/Rom.py
Normal file
@@ -0,0 +1,300 @@
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import struct
|
||||
import subprocess
|
||||
import copy
|
||||
import threading
|
||||
from .Utils import subprocess_args, data_path, get_version_bytes, __version__
|
||||
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.changed_address = {}
|
||||
self.changed_dma = {}
|
||||
self.force_patch = []
|
||||
|
||||
if file is None:
|
||||
return
|
||||
|
||||
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()}
|
||||
|
||||
# If decompressed file already exists, read from it
|
||||
if os.path.exists(decomp_file):
|
||||
file = decomp_file
|
||||
|
||||
if file == '':
|
||||
# if not specified, try to read from the previously decompressed rom
|
||||
file = decomp_file
|
||||
try:
|
||||
self.read_rom(file)
|
||||
except FileNotFoundError:
|
||||
# could not find the decompressed rom either
|
||||
raise FileNotFoundError('Must specify path to base ROM')
|
||||
else:
|
||||
self.read_rom(file)
|
||||
|
||||
# decompress rom, or check if it's already decompressed
|
||||
self.decompress_rom_file(file, decomp_file)
|
||||
|
||||
# Add file to maximum size
|
||||
self.buffer.extend(bytearray([0x00] * (0x4000000 - len(self.buffer))))
|
||||
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)
|
||||
new_rom.changed_address = copy.copy(self.changed_address)
|
||||
new_rom.changed_dma = copy.copy(self.changed_dma)
|
||||
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
|
||||
]
|
||||
|
||||
# Validate ROM file
|
||||
file_name = os.path.splitext(file)
|
||||
romCRC = list(self.buffer[0x10:0x18])
|
||||
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']:
|
||||
# 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:
|
||||
# If Input ROM is compressed, then Decompress it
|
||||
|
||||
sub_dir = data_path("Decompress")
|
||||
|
||||
if platform.system() == 'Windows':
|
||||
subcall = [sub_dir + "\\Decompress.exe", file, decomp_file]
|
||||
elif platform.system() == 'Linux':
|
||||
if platform.uname()[4] == 'aarch64' or platform.uname()[4] == 'arm64':
|
||||
subcall = [sub_dir + "/Decompress_ARM64", file, decomp_file]
|
||||
else:
|
||||
subcall = [sub_dir + "/Decompress", file, decomp_file]
|
||||
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.')
|
||||
|
||||
if not os.path.exists(subcall[0]):
|
||||
raise RuntimeError(f'Decompressor does not exist! Please place it at {subcall[0]}.')
|
||||
subprocess.call(subcall, **subprocess_args())
|
||||
self.read_rom(decomp_file)
|
||||
else:
|
||||
# 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
|
||||
|
||||
def write_bytes(self, address, values):
|
||||
super().write_bytes(address, values)
|
||||
self.changed_address.update(zip(range(address, address + len(values)), values))
|
||||
|
||||
def restore(self):
|
||||
self.buffer = copy.copy(self.original.buffer)
|
||||
self.changed_address = {}
|
||||
self.changed_dma = {}
|
||||
self.force_patch = []
|
||||
self.last_address = None
|
||||
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:
|
||||
with open(file, 'rb') as stream:
|
||||
self.buffer = bytearray(stream.read())
|
||||
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
|
||||
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)
|
||||
while True:
|
||||
if dma_start == 0 and dma_end == 0:
|
||||
return None
|
||||
if dma_start == key:
|
||||
return dma_start, dma_end, dma_size
|
||||
cur += 0x10
|
||||
dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
|
||||
|
||||
def verify_dmadata(self):
|
||||
cur = DMADATA_START
|
||||
overlapping_records = []
|
||||
dma_data = []
|
||||
|
||||
while True:
|
||||
this_start, this_end, this_size = self._get_dmadata_record(cur)
|
||||
|
||||
if this_start == 0 and this_end == 0:
|
||||
break
|
||||
|
||||
dma_data.append((this_start, this_end, this_size))
|
||||
cur += 0x10
|
||||
|
||||
dma_data.sort(key=lambda v: v[0])
|
||||
|
||||
for i in range(0, len(dma_data) - 1):
|
||||
this_start, this_end, this_size = dma_data[i]
|
||||
next_start, next_end, next_size = dma_data[i + 1]
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
if len(overlapping_records) > 0:
|
||||
raise Exception("Overlapping DMA Data Records!\n%s" % \
|
||||
'\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
|
||||
def update_dmadata_record(self, key, start, end, from_file=None):
|
||||
cur, dma_data_end = self.get_dma_table_range()
|
||||
dma_index = 0
|
||||
dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
|
||||
while dma_start != key:
|
||||
if dma_start == 0 and dma_end == 0:
|
||||
break
|
||||
|
||||
cur += 0x10
|
||||
dma_index += 1
|
||||
dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
|
||||
|
||||
if cur >= (dma_data_end - 0x10):
|
||||
raise Exception('dmadata update failed: key {0:x} not found in dmadata and dma table is full.'.format(key))
|
||||
else:
|
||||
self.write_int32s(cur, [start, end, start, 0])
|
||||
if from_file == None:
|
||||
if key == None:
|
||||
from_file = -1
|
||||
else:
|
||||
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)
|
||||
while True:
|
||||
if dma_start == 0 and dma_end == 0:
|
||||
raise Exception('Bad DMA Table: DMA Table entry missing.')
|
||||
|
||||
if dma_start == DMADATA_START:
|
||||
return (DMADATA_START, dma_end)
|
||||
|
||||
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
|
||||
def scan_dmadata_update(self):
|
||||
cur = DMADATA_START
|
||||
dma_data_end = None
|
||||
dma_index = 0
|
||||
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)
|
||||
|
||||
while True:
|
||||
if (dma_start == 0 and dma_end == 0) and \
|
||||
(old_dma_start == 0 and old_dma_end == 0):
|
||||
break
|
||||
|
||||
# If the entries do not match, the flag the changed entry
|
||||
if not (dma_start == old_dma_start and dma_end == old_dma_end):
|
||||
self.changed_dma[dma_index] = (-1, dma_start, dma_end - dma_start)
|
||||
|
||||
cur += 0x10
|
||||
dma_index += 1
|
||||
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
|
||||
max_end = 0
|
||||
|
||||
while True:
|
||||
this_start, this_end, this_size = self._get_dmadata_record(cur)
|
||||
|
||||
if this_start == 0 and this_end == 0:
|
||||
break
|
||||
|
||||
max_end = max(max_end, this_end)
|
||||
cur += 0x10
|
||||
max_end = ((max_end + 0x0F) >> 4) << 4
|
||||
return max_end
|
||||
|
||||
|
||||
def compress_rom_file(input_file, output_file):
|
||||
subcall = []
|
||||
|
||||
compressor_path = data_path("Compress")
|
||||
|
||||
if platform.system() == 'Windows':
|
||||
compressor_path += "\\Compress.exe"
|
||||
elif platform.system() == 'Linux':
|
||||
if platform.uname()[4] == 'aarch64' or platform.uname()[4] == 'arm64':
|
||||
compressor_path += "/Compress_ARM64"
|
||||
else:
|
||||
compressor_path += "/Compress"
|
||||
elif platform.system() == 'Darwin':
|
||||
compressor_path += "/Compress.out"
|
||||
else:
|
||||
raise RuntimeError('Unsupported operating system for compression.')
|
||||
|
||||
if not os.path.exists(compressor_path):
|
||||
raise RuntimeError(f'Compressor does not exist! Please place it at {compressor_path}.')
|
||||
process = subprocess.call([compressor_path, input_file, output_file], **subprocess_args(include_stdout=False))
|
||||
507
worlds/oot/RuleParser.py
Normal file
507
worlds/oot/RuleParser.py
Normal file
@@ -0,0 +1,507 @@
|
||||
import ast
|
||||
from collections import defaultdict
|
||||
from inspect import signature, _ParameterKind
|
||||
import logging
|
||||
import re
|
||||
|
||||
from .Items import item_table
|
||||
from .Location import OOTLocation
|
||||
from .Regions import TimeOfDay, OOTRegion
|
||||
from BaseClasses import CollectionState as State
|
||||
from .Utils import data_path, read_json
|
||||
|
||||
from worlds.generic.Rules import set_rule
|
||||
|
||||
|
||||
escaped_items = {}
|
||||
for item in item_table:
|
||||
escaped_items[re.sub(r'[\'()[\]]', '', item.replace(' ', '_'))] = item
|
||||
|
||||
event_name = re.compile(r'\w+')
|
||||
# All generated lambdas must accept these keyword args!
|
||||
# For evaluation at a certain age (required as all rules are evaluated at a specific age)
|
||||
# or at a certain spot (can be omitted in many cases)
|
||||
# or at a specific time of day (often unused)
|
||||
kwarg_defaults = {
|
||||
# 'age': None,
|
||||
# 'spot': None,
|
||||
# 'tod': TimeOfDay.NONE,
|
||||
}
|
||||
|
||||
allowed_globals = {'TimeOfDay': TimeOfDay}
|
||||
|
||||
rule_aliases = {}
|
||||
nonaliases = set()
|
||||
|
||||
def load_aliases():
|
||||
j = read_json(data_path('LogicHelpers.json'))
|
||||
for s, repl in j.items():
|
||||
if '(' in s:
|
||||
rule, args = s[:-1].split('(', 1)
|
||||
args = [re.compile(r'\b%s\b' % a.strip()) for a in args.split(',')]
|
||||
else:
|
||||
rule = s
|
||||
args = ()
|
||||
rule_aliases[rule] = (args, repl)
|
||||
nonaliases = escaped_items.keys() - rule_aliases.keys()
|
||||
|
||||
|
||||
def isliteral(expr):
|
||||
return isinstance(expr, (ast.Num, ast.Str, ast.Bytes, ast.NameConstant))
|
||||
|
||||
|
||||
class Rule_AST_Transformer(ast.NodeTransformer):
|
||||
|
||||
def __init__(self, world, player):
|
||||
self.world = world
|
||||
self.player = player
|
||||
self.events = set()
|
||||
# map Region -> rule ast string -> item name
|
||||
self.replaced_rules = defaultdict(dict)
|
||||
# delayed rules need to keep: region name, ast node, event name
|
||||
self.delayed_rules = []
|
||||
# lazy load aliases
|
||||
if not rule_aliases:
|
||||
load_aliases()
|
||||
# final rule cache
|
||||
self.rule_cache = {}
|
||||
self.kwarg_defaults = kwarg_defaults.copy() # otherwise this gets contaminated between players
|
||||
self.kwarg_defaults['player'] = self.player
|
||||
|
||||
|
||||
def visit_Name(self, node):
|
||||
if node.id in dir(self):
|
||||
return getattr(self, node.id)(node)
|
||||
elif node.id in rule_aliases:
|
||||
args, repl = rule_aliases[node.id]
|
||||
if args:
|
||||
raise Exception('Parse Error: expected %d args for %s, not 0' % (len(args), node.id),
|
||||
self.current_spot.name, ast.dump(node, False))
|
||||
return self.visit(ast.parse(repl, mode='eval').body)
|
||||
elif node.id in escaped_items:
|
||||
return ast.Call(
|
||||
func=ast.Attribute(
|
||||
value=ast.Name(id='state', ctx=ast.Load()),
|
||||
attr='has',
|
||||
ctx=ast.Load()),
|
||||
args=[ast.Str(escaped_items[node.id]), ast.Constant(self.player)],
|
||||
keywords=[])
|
||||
elif node.id in self.world.__dict__:
|
||||
# Settings are constant
|
||||
return ast.parse('%r' % self.world.__dict__[node.id], mode='eval').body
|
||||
elif node.id in State.__dict__:
|
||||
return self.make_call(node, node.id, [], [])
|
||||
elif node.id in self.kwarg_defaults or node.id in allowed_globals:
|
||||
return node
|
||||
elif event_name.match(node.id):
|
||||
self.events.add(node.id.replace('_', ' '))
|
||||
return ast.Call(
|
||||
func=ast.Attribute(
|
||||
value=ast.Name(id='state', ctx=ast.Load()),
|
||||
attr='has',
|
||||
ctx=ast.Load()),
|
||||
args=[ast.Str(node.id.replace('_', ' ')), ast.Constant(self.player)],
|
||||
keywords=[])
|
||||
else:
|
||||
raise Exception('Parse Error: invalid node name %s' % node.id, self.current_spot.name, ast.dump(node, False))
|
||||
|
||||
def visit_Str(self, node):
|
||||
return ast.Call(
|
||||
func=ast.Attribute(
|
||||
value=ast.Name(id='state', ctx=ast.Load()),
|
||||
attr='has',
|
||||
ctx=ast.Load()),
|
||||
args=[ast.Str(node.s), ast.Constant(self.player)],
|
||||
keywords=[])
|
||||
|
||||
# python 3.8 compatibility: ast walking now uses visit_Constant for Constant subclasses
|
||||
# this includes Num, Str, NameConstant, Bytes, and Ellipsis. We only handle Str.
|
||||
def visit_Constant(self, node):
|
||||
if isinstance(node, ast.Str):
|
||||
return self.visit_Str(node)
|
||||
return node
|
||||
|
||||
|
||||
def visit_Tuple(self, node):
|
||||
if len(node.elts) != 2:
|
||||
raise Exception('Parse Error: Tuple must have 2 values', self.current_spot.name, ast.dump(node, False))
|
||||
|
||||
item, count = node.elts
|
||||
|
||||
if not isinstance(item, (ast.Name, ast.Str)):
|
||||
raise Exception('Parse Error: first value must be an item. Got %s' % item.__class__.__name__, self.current_spot.name, ast.dump(node, False))
|
||||
iname = item.id if isinstance(item, ast.Name) else item.s
|
||||
|
||||
if not (isinstance(count, ast.Name) or isinstance(count, ast.Num)):
|
||||
raise Exception('Parse Error: second value must be a number. Got %s' % item.__class__.__name__, self.current_spot.name, ast.dump(node, False))
|
||||
|
||||
if isinstance(count, ast.Name):
|
||||
# Must be a settings constant
|
||||
count = ast.parse('%r' % self.world.__dict__[count.id], mode='eval').body
|
||||
|
||||
if iname in escaped_items:
|
||||
iname = escaped_items[iname]
|
||||
|
||||
if iname not in item_table:
|
||||
self.events.add(iname)
|
||||
|
||||
return ast.Call(
|
||||
func=ast.Attribute(
|
||||
value=ast.Name(id='state', ctx=ast.Load()),
|
||||
attr='has',
|
||||
ctx=ast.Load()),
|
||||
args=[ast.Str(iname), ast.Constant(self.player), count],
|
||||
keywords=[])
|
||||
|
||||
|
||||
def visit_Call(self, node):
|
||||
if not isinstance(node.func, ast.Name):
|
||||
return node
|
||||
|
||||
if node.func.id in dir(self):
|
||||
return getattr(self, node.func.id)(node)
|
||||
elif node.func.id in rule_aliases:
|
||||
args, repl = rule_aliases[node.func.id]
|
||||
if len(args) != len(node.args):
|
||||
raise Exception('Parse Error: expected %d args for %s, not %d' % (len(args), node.func.id, len(node.args)),
|
||||
self.current_spot.name, ast.dump(node, False))
|
||||
# straightforward string manip
|
||||
for arg_re, arg_val in zip(args, node.args):
|
||||
if isinstance(arg_val, ast.Name):
|
||||
val = arg_val.id
|
||||
elif isinstance(arg_val, ast.Constant):
|
||||
val = repr(arg_val.value)
|
||||
elif isinstance(arg_val, ast.Str):
|
||||
val = repr(arg_val.s)
|
||||
else:
|
||||
raise Exception('Parse Error: invalid argument %s' % ast.dump(arg_val, False),
|
||||
self.current_spot.name, ast.dump(node, False))
|
||||
repl = arg_re.sub(val, repl)
|
||||
return self.visit(ast.parse(repl, mode='eval').body)
|
||||
|
||||
new_args = []
|
||||
for child in node.args:
|
||||
if isinstance(child, ast.Name):
|
||||
if child.id in self.world.__dict__:
|
||||
# child = ast.Attribute(
|
||||
# value=ast.Attribute(
|
||||
# value=ast.Name(id='state', ctx=ast.Load()),
|
||||
# attr='world',
|
||||
# ctx=ast.Load()),
|
||||
# attr=child.id,
|
||||
# ctx=ast.Load())
|
||||
child = ast.Constant(getattr(self.world, child.id))
|
||||
elif child.id in rule_aliases:
|
||||
child = self.visit(child)
|
||||
elif child.id in escaped_items:
|
||||
child = ast.Str(escaped_items[child.id])
|
||||
else:
|
||||
child = ast.Str(child.id.replace('_', ' '))
|
||||
elif not isinstance(child, ast.Str):
|
||||
child = self.visit(child)
|
||||
new_args.append(child)
|
||||
|
||||
return self.make_call(node, node.func.id, new_args, node.keywords)
|
||||
|
||||
|
||||
def visit_Subscript(self, node):
|
||||
if isinstance(node.value, ast.Name):
|
||||
s = node.slice if isinstance(node.slice, ast.Name) else node.slice.value
|
||||
return ast.Subscript(
|
||||
value=ast.Attribute(
|
||||
# value=ast.Attribute(
|
||||
# value=ast.Name(id='state', ctx=ast.Load()),
|
||||
# attr='world',
|
||||
# ctx=ast.Load()),
|
||||
value=ast.Subscript(
|
||||
value=ast.Attribute(
|
||||
value=ast.Attribute(
|
||||
value=ast.Name(id='state', ctx=ast.Load()),
|
||||
attr='world',
|
||||
ctx=ast.Load()),
|
||||
attr='worlds',
|
||||
ctx=ast.Load()),
|
||||
slice=ast.Index(value=ast.Constant(self.player)),
|
||||
ctx=ast.Load()),
|
||||
attr=node.value.id,
|
||||
ctx=ast.Load()),
|
||||
slice=ast.Index(value=ast.Str(s.id.replace('_', ' '))),
|
||||
ctx=node.ctx)
|
||||
else:
|
||||
return node
|
||||
|
||||
|
||||
def visit_Compare(self, node):
|
||||
def escape_or_string(n):
|
||||
if isinstance(n, ast.Name) and n.id in escaped_items:
|
||||
return ast.Str(escaped_items[n.id])
|
||||
elif not isinstance(n, ast.Str):
|
||||
return self.visit(n)
|
||||
return n
|
||||
|
||||
# Fast check for json can_use
|
||||
if (len(node.ops) == 1 and isinstance(node.ops[0], ast.Eq)
|
||||
and isinstance(node.left, ast.Name) and isinstance(node.comparators[0], ast.Name)
|
||||
and node.left.id not in self.world.__dict__ and node.comparators[0].id not in self.world.__dict__):
|
||||
return ast.NameConstant(node.left.id == node.comparators[0].id)
|
||||
|
||||
node.left = escape_or_string(node.left)
|
||||
node.comparators = list(map(escape_or_string, node.comparators))
|
||||
node.ops = list(map(self.visit, node.ops))
|
||||
|
||||
# if all the children are literals now, we can evaluate
|
||||
if isliteral(node.left) and all(map(isliteral, node.comparators)):
|
||||
# either we turn the ops into operator functions to apply (lots of work),
|
||||
# or we compile, eval, and reparse the result
|
||||
try:
|
||||
res = eval(compile(ast.fix_missing_locations(ast.Expression(node)), '<string>', 'eval'))
|
||||
except TypeError as e:
|
||||
raise Exception('Parse Error: %s' % e, self.current_spot.name, ast.dump(node, False))
|
||||
return self.visit(ast.parse('%r' % res, mode='eval').body)
|
||||
return node
|
||||
|
||||
|
||||
def visit_UnaryOp(self, node):
|
||||
# visit the children first
|
||||
self.generic_visit(node)
|
||||
# if all the children are literals now, we can evaluate
|
||||
if isliteral(node.operand):
|
||||
res = eval(compile(ast.Expression(node), '<string>', 'eval'))
|
||||
return ast.parse('%r' % res, mode='eval').body
|
||||
return node
|
||||
|
||||
|
||||
def visit_BinOp(self, node):
|
||||
# visit the children first
|
||||
self.generic_visit(node)
|
||||
# if all the children are literals now, we can evaluate
|
||||
if isliteral(node.left) and isliteral(node.right):
|
||||
res = eval(compile(ast.Expression(node), '<string>', 'eval'))
|
||||
return ast.parse('%r' % res, mode='eval').body
|
||||
return node
|
||||
|
||||
|
||||
def visit_BoolOp(self, node):
|
||||
# Everything else must be visited, then can be removed/reduced to.
|
||||
early_return = isinstance(node.op, ast.Or)
|
||||
groupable = 'has_any' if early_return else 'has_all'
|
||||
items = set()
|
||||
new_values = []
|
||||
# if any elt is True(And)/False(Or), we can omit it
|
||||
# if any is False(And)/True(Or), the whole node can be replaced with it
|
||||
for elt in list(node.values):
|
||||
if isinstance(elt, ast.Str):
|
||||
items.add(elt.s)
|
||||
elif isinstance(elt, ast.Name) and elt.id in nonaliases:
|
||||
items.add(escaped_items[elt.id])
|
||||
else:
|
||||
# It's possible this returns a single item check,
|
||||
# but it's already wrapped in a Call.
|
||||
elt = self.visit(elt)
|
||||
if isinstance(elt, ast.NameConstant):
|
||||
if elt.value == early_return:
|
||||
return elt
|
||||
# else omit it
|
||||
elif (isinstance(elt, ast.Call) and isinstance(elt.func, ast.Attribute)
|
||||
and elt.func.attr in ('has', groupable) and len(elt.args) == 1):
|
||||
args = elt.args[0]
|
||||
if isinstance(args, ast.Str):
|
||||
items.add(args.s)
|
||||
else:
|
||||
items.update(it.s for it in args.elts)
|
||||
elif isinstance(elt, ast.BoolOp) and node.op.__class__ == elt.op.__class__:
|
||||
new_values.extend(elt.values)
|
||||
else:
|
||||
new_values.append(elt)
|
||||
|
||||
# package up the remaining items and values
|
||||
if not items and not new_values:
|
||||
# all values were True(And)/False(Or)
|
||||
return ast.NameConstant(not early_return)
|
||||
|
||||
if items:
|
||||
node.values = [ast.Call(
|
||||
func=ast.Attribute(
|
||||
value=ast.Name(id='state', ctx=ast.Load()),
|
||||
attr='has_any' if early_return else 'has_all',
|
||||
ctx=ast.Load()),
|
||||
args=[ast.Tuple(elts=[ast.Str(i) for i in items], ctx=ast.Load()), ast.Constant(self.player)],
|
||||
keywords=[])] + new_values
|
||||
else:
|
||||
node.values = new_values
|
||||
if len(node.values) == 1:
|
||||
return node.values[0]
|
||||
return node
|
||||
|
||||
|
||||
# Generates an ast.Call invoking the given State function 'name',
|
||||
# providing given args and keywords, and adding in additional
|
||||
# keyword args from kwarg_defaults (age, etc.)
|
||||
def make_call(self, node, name, args, keywords):
|
||||
if not hasattr(State, name):
|
||||
raise Exception('Parse Error: No such function State.%s' % name, self.current_spot.name, ast.dump(node, False))
|
||||
|
||||
for (k, v) in self.kwarg_defaults.items():
|
||||
keywords.append(ast.keyword(arg=f'{k}', value=ast.Constant(v)))
|
||||
|
||||
return ast.Call(
|
||||
func=ast.Attribute(
|
||||
value=ast.Name(id='state', ctx=ast.Load()),
|
||||
attr=name,
|
||||
ctx=ast.Load()),
|
||||
args=args,
|
||||
keywords=keywords)
|
||||
|
||||
|
||||
def replace_subrule(self, target, node):
|
||||
rule = ast.dump(node, False)
|
||||
if rule in self.replaced_rules[target]:
|
||||
return self.replaced_rules[target][rule]
|
||||
|
||||
subrule_name = target + ' Subrule %d' % (1 + len(self.replaced_rules[target]))
|
||||
# Save the info to be made into a rule later
|
||||
self.delayed_rules.append((target, node, subrule_name))
|
||||
# Replace the call with a reference to that item
|
||||
item_rule = ast.Call(
|
||||
func=ast.Attribute(
|
||||
value=ast.Name(id='state', ctx=ast.Load()),
|
||||
attr='has',
|
||||
ctx=ast.Load()),
|
||||
args=[ast.Str(subrule_name), ast.Constant(self.player)],
|
||||
keywords=[])
|
||||
# Cache the subrule for any others in this region
|
||||
# (and reserve the item name in the process)
|
||||
self.replaced_rules[target][rule] = item_rule
|
||||
return item_rule
|
||||
|
||||
|
||||
# Requires the target regions have been defined in the world.
|
||||
def create_delayed_rules(self):
|
||||
for region_name, node, subrule_name in self.delayed_rules:
|
||||
region = self.world.world.get_region(region_name, self.player)
|
||||
event = OOTLocation(self.player, subrule_name, type='Event', parent=region, internal=True)
|
||||
event.show_in_spoiler = False
|
||||
|
||||
self.current_spot = event
|
||||
# This could, in theory, create further subrules.
|
||||
access_rule = self.make_access_rule(self.visit(node))
|
||||
if access_rule is self.rule_cache.get('NameConstant(False)'):
|
||||
event.access_rule = None
|
||||
event.never = True
|
||||
logging.getLogger('').debug('Dropping unreachable delayed event: %s', event.name)
|
||||
else:
|
||||
if access_rule is self.rule_cache.get('NameConstant(True)'):
|
||||
event.always = True
|
||||
set_rule(event, access_rule)
|
||||
region.locations.append(event)
|
||||
|
||||
self.world.make_event_item(subrule_name, event)
|
||||
# Safeguard in case this is called multiple times per world
|
||||
self.delayed_rules.clear()
|
||||
|
||||
|
||||
def make_access_rule(self, body):
|
||||
rule_str = ast.dump(body, False)
|
||||
if rule_str not in self.rule_cache:
|
||||
# requires consistent iteration on dicts
|
||||
kwargs = [ast.arg(arg=k) for k in self.kwarg_defaults.keys()]
|
||||
kwd = list(map(ast.Constant, self.kwarg_defaults.values()))
|
||||
try:
|
||||
self.rule_cache[rule_str] = eval(compile(
|
||||
ast.fix_missing_locations(
|
||||
ast.Expression(ast.Lambda(
|
||||
args=ast.arguments(
|
||||
posonlyargs=[],
|
||||
args=[ast.arg(arg='state')],
|
||||
defaults=[],
|
||||
kwonlyargs=kwargs,
|
||||
kw_defaults=kwd),
|
||||
body=body))),
|
||||
'<string>', 'eval'),
|
||||
# globals/locals. if undefined, everything in the namespace *now* would be allowed
|
||||
allowed_globals)
|
||||
except TypeError as e:
|
||||
raise Exception('Parse Error: %s' % e, self.current_spot.name, ast.dump(body, False))
|
||||
return self.rule_cache[rule_str]
|
||||
|
||||
|
||||
## Handlers for specific internal functions used in the json logic.
|
||||
|
||||
# at(region_name, rule)
|
||||
# Creates an internal event at the remote region and depends on it.
|
||||
def at(self, node):
|
||||
# Cache this under the target (region) name
|
||||
if len(node.args) < 2 or not isinstance(node.args[0], ast.Str):
|
||||
raise Exception('Parse Error: invalid at() arguments', self.current_spot.name, ast.dump(node, False))
|
||||
return self.replace_subrule(node.args[0].s, node.args[1])
|
||||
|
||||
|
||||
# here(rule)
|
||||
# Creates an internal event in the same region and depends on it.
|
||||
def here(self, node):
|
||||
if not node.args:
|
||||
raise Exception('Parse Error: missing here() argument', self.current_spot.name, ast.dump(node, False))
|
||||
return self.replace_subrule(
|
||||
self.current_spot.parent_region.name,
|
||||
node.args[0])
|
||||
|
||||
## Handlers for compile-time optimizations (former State functions)
|
||||
|
||||
def at_day(self, node):
|
||||
if self.world.ensure_tod_access:
|
||||
# tod has DAY or (tod == NONE and (ss or find a path from a provider))
|
||||
# parsing is better than constructing this expression by hand
|
||||
return ast.parse("(tod & TimeOfDay.DAY) if tod else (state.has_all(('Ocarina', 'Suns Song')) or state.search.can_reach(spot.parent_region, age=age, tod=TimeOfDay.DAY))", mode='eval').body
|
||||
return ast.NameConstant(True)
|
||||
|
||||
def at_dampe_time(self, node):
|
||||
if self.world.ensure_tod_access:
|
||||
# tod has DAMPE or (tod == NONE and (find a path from a provider))
|
||||
# parsing is better than constructing this expression by hand
|
||||
return ast.parse("(tod & TimeOfDay.DAMPE) if tod else state.search.can_reach(spot.parent_region, age=age, tod=TimeOfDay.DAMPE)", mode='eval').body
|
||||
return ast.NameConstant(True)
|
||||
|
||||
def at_night(self, node):
|
||||
if self.current_spot.type == 'GS Token' and self.world.logic_no_night_tokens_without_suns_song:
|
||||
# Using visit here to resolve 'can_play' rule
|
||||
return self.visit(ast.parse('can_play(Suns_Song)', mode='eval').body)
|
||||
if self.world.ensure_tod_access:
|
||||
# tod has DAMPE or (tod == NONE and (ss or find a path from a provider))
|
||||
# parsing is better than constructing this expression by hand
|
||||
return ast.parse("(tod & TimeOfDay.DAMPE) if tod else (state.has_all(('Ocarina', 'Suns Song')) or state.search.can_reach(spot.parent_region, age=age, tod=TimeOfDay.DAMPE))", mode='eval').body
|
||||
return ast.NameConstant(True)
|
||||
|
||||
|
||||
# Parse entry point
|
||||
# If spot is None, here() rules won't work.
|
||||
def parse_rule(self, rule_string, spot=None):
|
||||
self.current_spot = spot
|
||||
return self.make_access_rule(self.visit(ast.parse(rule_string, mode='eval').body))
|
||||
|
||||
def parse_spot_rule(self, spot):
|
||||
rule = spot.rule_string.split('#', 1)[0].strip()
|
||||
|
||||
access_rule = self.parse_rule(rule, spot)
|
||||
set_rule(spot, access_rule)
|
||||
if access_rule is self.rule_cache.get('NameConstant(False)'):
|
||||
spot.never = True
|
||||
elif access_rule is self.rule_cache.get('NameConstant(True)'):
|
||||
spot.always = True
|
||||
|
||||
# Hijacking functions
|
||||
def current_spot_child_access(self, node):
|
||||
r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
|
||||
return ast.parse(f"state._oot_reach_as_age('{r.name}', 'child', {self.player})", mode='eval').body
|
||||
|
||||
def current_spot_adult_access(self, node):
|
||||
r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
|
||||
return ast.parse(f"state._oot_reach_as_age('{r.name}', 'adult', {self.player})", mode='eval').body
|
||||
|
||||
def current_spot_starting_age_access(self, node):
|
||||
return self.current_spot_child_access(node) if self.world.starting_age == 'child' else self.current_spot_adult_access(node)
|
||||
|
||||
def has_bottle(self, node):
|
||||
return ast.parse(f"state._oot_has_bottle({self.player})", mode='eval').body
|
||||
|
||||
def can_live_dmg(self, node):
|
||||
return ast.parse(f"state._oot_can_live_dmg({self.player}, {node.args[0].value})", mode='eval').body
|
||||
203
worlds/oot/Rules.py
Normal file
203
worlds/oot/Rules.py
Normal file
@@ -0,0 +1,203 @@
|
||||
from collections import deque
|
||||
import logging
|
||||
|
||||
from .SaveContext import SaveContext
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item, item_in_locations
|
||||
from ..AutoWorld import LogicMixin
|
||||
|
||||
|
||||
class OOTLogic(LogicMixin):
|
||||
|
||||
def _oot_has_stones(self, count, player):
|
||||
return self.has_group("stones", player, count)
|
||||
|
||||
def _oot_has_medallions(self, count, player):
|
||||
return self.has_group("medallions", player, count)
|
||||
|
||||
def _oot_has_dungeon_rewards(self, count, player):
|
||||
return self.has_group("rewards", player, count)
|
||||
|
||||
def _oot_has_bottle(self, player):
|
||||
return self.has_group("bottles", player)
|
||||
|
||||
# Used for fall damage and other situations where damage is unavoidable
|
||||
def _oot_can_live_dmg(self, player, hearts):
|
||||
mult = self.world.worlds[player].damage_multiplier
|
||||
if hearts*4 >= 3:
|
||||
return mult != 'ohko' and mult != 'quadruple'
|
||||
elif hearts*4 < 3:
|
||||
return mult != 'ohko'
|
||||
else:
|
||||
return True
|
||||
|
||||
# This function operates by assuming different behavior based on the "level of recursion", handled manually.
|
||||
# If it's called while self.age[player] is None, then it will set the age variable and then attempt to reach the region.
|
||||
# If self.age[player] is not None, then it will compare it to the 'age' parameter, and return True iff they are equal.
|
||||
# This lets us fake the OOT accessibility check that cares about age. Unfortunately it's still tied to the ground region.
|
||||
def _oot_reach_as_age(self, regionname, age, player):
|
||||
if self.age[player] is None:
|
||||
self.age[player] = age
|
||||
can_reach = self.world.get_region(regionname, player).can_reach(self)
|
||||
self.age[player] = None
|
||||
return can_reach
|
||||
return self.age[player] == age
|
||||
|
||||
# Store the age before calling this!
|
||||
def _oot_update_age_reachable_regions(self, player):
|
||||
self.stale[player] = False
|
||||
for age in ['child', 'adult']:
|
||||
self.age[player] = age
|
||||
rrp = getattr(self, f'{age}_reachable_regions')[player]
|
||||
bc = getattr(self, f'{age}_blocked_connections')[player]
|
||||
queue = deque(getattr(self, f'{age}_blocked_connections')[player])
|
||||
start = self.world.get_region('Menu', player)
|
||||
|
||||
# init on first call - this can't be done on construction since the regions don't exist yet
|
||||
if not start in rrp:
|
||||
rrp.add(start)
|
||||
bc.update(start.exits)
|
||||
queue.extend(start.exits)
|
||||
|
||||
# run BFS on all connections, and keep track of those blocked by missing items
|
||||
while queue:
|
||||
connection = queue.popleft()
|
||||
new_region = connection.connected_region
|
||||
if new_region in rrp:
|
||||
bc.remove(connection)
|
||||
elif connection.can_reach(self):
|
||||
rrp.add(new_region)
|
||||
bc.remove(connection)
|
||||
bc.update(new_region.exits)
|
||||
queue.extend(new_region.exits)
|
||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||
|
||||
|
||||
def set_rules(ootworld):
|
||||
logger = logging.getLogger('')
|
||||
|
||||
world = ootworld.world
|
||||
player = ootworld.player
|
||||
|
||||
if ootworld.logic_rules != 'no_logic':
|
||||
if ootworld.triforce_hunt:
|
||||
world.completion_condition[player] = lambda state: state.has('Triforce Piece', player, ootworld.triforce_goal)
|
||||
else:
|
||||
world.completion_condition[player] = lambda state: state.has('Triforce', player)
|
||||
|
||||
# ganon can only carry triforce
|
||||
world.get_location('Ganon', player).item_rule = lambda item: item.name == 'Triforce'
|
||||
|
||||
# is_child = ootworld.parser.parse_rule('is_child')
|
||||
# guarantee_hint = ootworld.parser.parse_rule('guarantee_hint')
|
||||
|
||||
for location in ootworld.get_locations():
|
||||
if ootworld.shuffle_song_items == 'song':
|
||||
if location.type == 'Song':
|
||||
# must be a song, or there are songs in starting items; then it can be anything
|
||||
add_item_rule(location, lambda item:
|
||||
(ootworld.starting_songs and item.type != 'Song')
|
||||
or (item.type == 'Song' and item.player == location.player))
|
||||
else:
|
||||
add_item_rule(location, lambda item: item.type != 'Song')
|
||||
|
||||
if location.type == 'Shop':
|
||||
if location.name in ootworld.shop_prices:
|
||||
add_item_rule(location, lambda item: item.type != 'Shop')
|
||||
location.price = ootworld.shop_prices[location.name]
|
||||
add_rule(location, create_shop_rule(location, ootworld.parser))
|
||||
else:
|
||||
add_item_rule(location, lambda item: item.type == 'Shop' and item.player == location.player)
|
||||
elif 'Deku Scrub' in location.name:
|
||||
add_rule(location, create_shop_rule(location, ootworld.parser))
|
||||
else:
|
||||
add_item_rule(location, lambda item: item.type != 'Shop')
|
||||
|
||||
if ootworld.skip_child_zelda and location.name == 'Song from Impa':
|
||||
limit_to_itemset(location, SaveContext.giveable_items)
|
||||
add_item_rule(location, lambda item: item.player == location.player)
|
||||
|
||||
if location.name == 'Forest Temple MQ First Room Chest' and ootworld.shuffle_bosskeys == 'dungeon' and ootworld.shuffle_smallkeys == 'dungeon' and ootworld.tokensanity == 'off':
|
||||
# This location needs to be a small key. Make sure the boss key isn't placed here.
|
||||
forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player)
|
||||
|
||||
# TODO: re-add hints once they are working
|
||||
# if location.type == 'HintStone' and ootworld.hints == 'mask':
|
||||
# location.add_rule(is_child)
|
||||
|
||||
# if location.name in ootworld.always_hints:
|
||||
# location.add_rule(guarantee_hint)
|
||||
|
||||
|
||||
def create_shop_rule(location, parser):
|
||||
def required_wallets(price):
|
||||
if price > 500:
|
||||
return 3
|
||||
if price > 200:
|
||||
return 2
|
||||
if price > 99:
|
||||
return 1
|
||||
return 0
|
||||
return parser.parse_rule('(Progressive_Wallet, %d)' % required_wallets(location.price))
|
||||
|
||||
|
||||
def limit_to_itemset(location, itemset):
|
||||
old_rule = location.item_rule
|
||||
location.item_rule = lambda item: item.name in itemset and old_rule(item)
|
||||
|
||||
|
||||
# This function should be run once after the shop items are placed in the world.
|
||||
# It should be run before other items are placed in the world so that logic has
|
||||
# the correct checks for them. This is safe to do since every shop is still
|
||||
# accessible when all items are obtained and every shop item is not.
|
||||
# This function should also be called when a world is copied if the original world
|
||||
# had called this function because the world.copy does not copy the rules
|
||||
def set_shop_rules(ootworld):
|
||||
found_bombchus = ootworld.parser.parse_rule('found_bombchus')
|
||||
wallet = ootworld.parser.parse_rule('Progressive_Wallet')
|
||||
wallet2 = ootworld.parser.parse_rule('(Progressive_Wallet, 2)')
|
||||
for location in ootworld.world.get_filled_locations():
|
||||
if location.player == ootworld.player and location.item.type == 'Shop':
|
||||
# Add wallet requirements
|
||||
if location.item.name in ['Buy Arrows (50)', 'Buy Fish', 'Buy Goron Tunic', 'Buy Bombchu (20)', 'Buy Bombs (30)']:
|
||||
add_rule(location, wallet)
|
||||
elif location.item.name in ['Buy Zora Tunic', 'Buy Blue Fire']:
|
||||
add_rule(location, wallet2)
|
||||
|
||||
# Add adult only checks
|
||||
if location.item.name in ['Buy Goron Tunic', 'Buy Zora Tunic']:
|
||||
is_adult = ootworld.parser.parse_rule('is_adult', location)
|
||||
add_rule(location, is_adult)
|
||||
|
||||
# Add item prerequisite checks
|
||||
if location.item.name in ['Buy Blue Fire',
|
||||
'Buy Blue Potion',
|
||||
'Buy Bottle Bug',
|
||||
'Buy Fish',
|
||||
'Buy Green Potion',
|
||||
'Buy Poe',
|
||||
'Buy Red Potion [30]',
|
||||
'Buy Red Potion [40]',
|
||||
'Buy Red Potion [50]',
|
||||
'Buy Fairy\'s Spirit']:
|
||||
add_rule(location, lambda state: CollectionState._oot_has_bottle(state, ootworld.player))
|
||||
if location.item.name in ['Buy Bombchu (10)', 'Buy Bombchu (20)', 'Buy Bombchu (5)']:
|
||||
add_rule(location, found_bombchus)
|
||||
|
||||
|
||||
# This function should be ran once after setting up entrances and before placing items
|
||||
# The goal is to automatically set item rules based on age requirements in case entrances were shuffled
|
||||
def set_entrances_based_rules(ootworld):
|
||||
|
||||
if ootworld.world.accessibility == 'beatable':
|
||||
return
|
||||
|
||||
all_state = ootworld.state_with_items(ootworld.itempool)
|
||||
|
||||
for location in ootworld.get_locations():
|
||||
# If a shop is not reachable as adult, it can't have Goron Tunic or Zora Tunic as child can't buy these
|
||||
if location.type == 'Shop' and not all_state._oot_reach_as_age(location.parent_region.name, 'adult', ootworld.player):
|
||||
forbid_item(location, 'Buy Goron Tunic', ootworld.player)
|
||||
forbid_item(location, 'Buy Zora Tunic', ootworld.player)
|
||||
|
||||
1000
worlds/oot/SaveContext.py
Normal file
1000
worlds/oot/SaveContext.py
Normal file
File diff suppressed because it is too large
Load Diff
212
worlds/oot/Sounds.py
Normal file
212
worlds/oot/Sounds.py
Normal file
@@ -0,0 +1,212 @@
|
||||
# SOUNDS.PY
|
||||
#
|
||||
# A data-oriented module created to avoid cluttering (and entangling) other,
|
||||
# more important modules with sound data.
|
||||
#
|
||||
# Tags
|
||||
# To easily fetch related sounds by their properties. This seems generally
|
||||
# better than the alternative of defining long lists by hand. You still can, of
|
||||
# course. Categorizing sounds with more useful tags will require some work. Do
|
||||
# this as needed.
|
||||
#
|
||||
# Sounds
|
||||
# These are a collection of data structures relating to sounds. Already I'm sure
|
||||
# you get the picture.
|
||||
#
|
||||
# Sound Pools
|
||||
# These are just groups of sounds, to be referenced by sfx settings. Could
|
||||
# potentially merit enumerating later on. ¯\_(ツ)_/¯
|
||||
#
|
||||
# Sound Hooks
|
||||
# These are intended to gear themselves toward configurable settings, rather
|
||||
# than to document every location where a particular sound is used. For example,
|
||||
# suppose we want a setting to override all of Link's vocalizations. The sound
|
||||
# hook would contain a bunch of addresses, whether they share the same default
|
||||
# value or not.
|
||||
|
||||
from enum import Enum
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
class Tags(Enum):
|
||||
LOOPED = 0
|
||||
QUIET = 1
|
||||
IMMEDIATE = 2 # Delayed sounds are commonly undesirable
|
||||
BRIEF = 3 # Punchy sounds, good for rapid fire
|
||||
NEW = 4
|
||||
PAINFUL = 5 # Eardrum-piercing sounds
|
||||
NAVI = 6 # Navi sounds (hand chosen)
|
||||
HPLOW = 7 # Low HP sounds (hand chosen)
|
||||
HOVERBOOT = 8 # Hover boot sounds (hand chosen)
|
||||
NIGHTFALL = 9 # Nightfall sounds (hand chosen)
|
||||
MENUSELECT = 10 # Menu selection sounds (hand chosen, could use some more)
|
||||
MENUMOVE = 11 # Menu movement sounds (hand chosen, could use some more)
|
||||
HORSE = 12 # Horse neigh sounds (hand chosen)
|
||||
INC_NE = 20 # Incompatible with NAVI_ENEMY? (Verify)
|
||||
# I'm now thinking it has to do with a limit of concurrent sounds)
|
||||
|
||||
Sound = namedtuple('Sound', 'id keyword label tags')
|
||||
class Sounds(Enum):
|
||||
NONE = Sound(0x0000, 'none', 'None', [Tags.NAVI, Tags.HPLOW])
|
||||
ARMOS_GROAN = Sound(0x3848, 'armos', 'Armos', [Tags.HORSE, Tags.PAINFUL])
|
||||
BARK = Sound(0x28D8, 'bark', 'Bark', [Tags.BRIEF, Tags.NAVI, Tags.HPLOW, Tags.HOVERBOOT])
|
||||
BOMB_BOUNCE = Sound(0x282F, 'bomb-bounce', 'Bomb Bounce', [Tags.QUIET, Tags.HPLOW])
|
||||
BONGO_HIGH = Sound(0x3951, 'bongo-bongo-high', 'Bongo Bongo High', [Tags.MENUSELECT])
|
||||
BONGO_LOW = Sound(0x3950, 'bongo-bongo-low', 'Bongo Bongo Low', [Tags.QUIET, Tags.HPLOW, Tags.MENUMOVE])
|
||||
BOTTLE_CORK = Sound(0x286C, 'bottle-cork', 'Bottle Cork', [Tags.IMMEDIATE, Tags.BRIEF, Tags.QUIET])
|
||||
BOW_TWANG = Sound(0x1830, 'bow-twang', 'Bow Twang', [Tags.HPLOW, Tags.MENUMOVE])
|
||||
BUBBLE_LOL = Sound(0x38CA, 'bubble-laugh', 'Bubble Laugh', [])
|
||||
BUSINESS_SCRUB = Sound(0x3882, 'business-scrub', 'Business Scrub', [Tags.PAINFUL, Tags.NAVI, Tags.HPLOW])
|
||||
CARROT_REFILL = Sound(0x4845, 'carrot-refill', 'Carrot Refill', [Tags.NAVI, Tags.HPLOW])
|
||||
CARTOON_FALL = Sound(0x28A0, 'cartoon-fall', 'Cartoon Fall', [Tags.PAINFUL, Tags.HOVERBOOT])
|
||||
CHANGE_ITEM = Sound(0x0835, 'change-item', 'Change Item', [Tags.IMMEDIATE, Tags.BRIEF, Tags.MENUSELECT])
|
||||
CHEST_OPEN = Sound(0x2820, 'chest-open', 'Chest Open', [Tags.PAINFUL])
|
||||
CHILD_CRINGE = Sound(0x683A, 'child-cringe', 'Child Cringe', [Tags.PAINFUL, Tags.IMMEDIATE, Tags.MENUSELECT])
|
||||
CHILD_GASP = Sound(0x6836, 'child-gasp', 'Child Gasp', [Tags.PAINFUL])
|
||||
CHILD_HURT = Sound(0x6825, 'child-hurt', 'Child Hurt', [Tags.PAINFUL])
|
||||
CHILD_OWO = Sound(0x6823, 'child-owo', 'Child owo', [Tags.PAINFUL])
|
||||
CHILD_PANT = Sound(0x6829, 'child-pant', 'Child Pant', [Tags.IMMEDIATE])
|
||||
CHILD_SCREAM = Sound(0x6828, 'child-scream', 'Child Scream', [Tags.PAINFUL, Tags.IMMEDIATE, Tags.MENUSELECT, Tags.HORSE])
|
||||
CUCCO_CLUCK = Sound(0x2812, 'cluck', 'Cluck', [Tags.BRIEF, Tags.NAVI, Tags.HPLOW])
|
||||
CUCCO_CROW = Sound(0x2813, 'cockadoodledoo', 'Cockadoodledoo', [Tags.PAINFUL, Tags.NAVI, Tags.NIGHTFALL])
|
||||
CURSED_ATTACK = Sound(0x6868, 'cursed-attack', 'Cursed Attack', [Tags.PAINFUL, Tags.IMMEDIATE])
|
||||
CURSED_SCREAM = Sound(0x6867, 'cursed-scream', 'Cursed Scream', [Tags.PAINFUL])
|
||||
DEKU_BABA_CHATTER = Sound(0x3860, 'deku-baba', 'Deku Baba', [Tags.MENUMOVE])
|
||||
DRAWBRIDGE_SET = Sound(0x280E, 'drawbridge-set', 'Drawbridge Set', [Tags.HPLOW])
|
||||
DUSK_HOWL = Sound(0x28AE, 'dusk-howl', 'Dusk Howl', [Tags.NAVI])
|
||||
EPONA_CHILD = Sound(0x2844, 'baby-epona', 'Epona (Baby)', [Tags.PAINFUL])
|
||||
EXPLODE_CRATE = Sound(0x2839, 'exploding-crate', 'Exploding Crate', [Tags.PAINFUL, Tags.NAVI])
|
||||
EXPLOSION = Sound(0x180E, 'explosion', 'Explosion', [Tags.PAINFUL, Tags.NAVI])
|
||||
FANFARE_SMALL = Sound(0x4824, 'fanfare-light', 'Fanfare (Light)', [])
|
||||
FANFARE_MED = Sound(0x4831, 'fanfare-medium', 'Fanfare (Medium)', [])
|
||||
FIELD_SHRUB = Sound(0x2877, 'field-shrub', 'Field Shrub', [])
|
||||
FLARE_BOSS_LOL = Sound(0x3981, 'flare-dancer-laugh', 'Flare Dancer Laugh', [Tags.PAINFUL, Tags.IMMEDIATE, Tags.HOVERBOOT])
|
||||
FLARE_BOSS_STARTLE = Sound(0x398B, 'flare-dancer-startled', 'Flare Dancer Startled', [])
|
||||
GANON_TENNIS = Sound(0x39CA, 'ganondorf-teh', 'Ganondorf "Teh!"', [])
|
||||
GOHMA_LARVA_CROAK = Sound(0x395D, 'gohma-larva-croak', 'Gohma Larva Croak', [])
|
||||
GOLD_SKULL_TOKEN = Sound(0x4843, 'gold-skull-token', 'Gold Skull Token', [Tags.NIGHTFALL])
|
||||
GORON_WAKE = Sound(0x38FC, 'goron-wake', 'Goron Wake', [])
|
||||
GREAT_FAIRY = Sound(0x6858, 'great-fairy', 'Great Fairy', [Tags.PAINFUL, Tags.NAVI, Tags.NIGHTFALL, Tags.HORSE])
|
||||
GUAY = Sound(0x38B6, 'guay', 'Guay', [Tags.BRIEF, Tags.NAVI, Tags.HPLOW])
|
||||
GUNSHOT = Sound(0x4835, 'gunshot', 'Gunshot', [])
|
||||
HAMMER_BONK = Sound(0x180A, 'hammer-bonk', 'Hammer Bonk', [])
|
||||
HORSE_NEIGH = Sound(0x2805, 'horse-neigh', 'Horse Neigh', [Tags.PAINFUL, Tags.NAVI])
|
||||
HORSE_TROT = Sound(0x2804, 'horse-trot', 'Horse Trot', [Tags.HPLOW])
|
||||
HOVER_BOOTS = Sound(0x08C9, 'hover-boots', 'Hover Boots', [Tags.LOOPED, Tags.PAINFUL])
|
||||
HP_LOW = Sound(0x481B, 'low-health', 'HP Low', [Tags.INC_NE, Tags.NAVI])
|
||||
HP_RECOVER = Sound(0x480B, 'recover-health', 'HP Recover', [Tags.NAVI, Tags.HPLOW])
|
||||
ICE_SHATTER = Sound(0x0875, 'shattering-ice', 'Ice Shattering', [Tags.PAINFUL, Tags.NAVI])
|
||||
INGO_WOOAH = Sound(0x6854, 'ingo-wooah', 'Ingo "Wooah!"', [Tags.PAINFUL])
|
||||
IRON_BOOTS = Sound(0x080D, 'iron-boots', 'Iron Boots', [Tags.BRIEF, Tags.HPLOW, Tags.QUIET])
|
||||
IRON_KNUCKLE = Sound(0x3929, 'iron-knuckle', 'Iron Knuckle', [])
|
||||
INGO_KAAH = Sound(0x6855, 'kaah', 'Kaah!', [Tags.PAINFUL])
|
||||
MOBLIN_CLUB_GROUND = Sound(0x38E1, 'moblin-club-ground', 'Moblin Club Ground', [Tags.PAINFUL])
|
||||
MOBLIN_CLUB_SWING = Sound(0x39EF, 'moblin-club-swing', 'Moblin Club Swing', [Tags.PAINFUL])
|
||||
MOO = Sound(0x28DF, 'moo', 'Moo', [Tags.NAVI, Tags.NIGHTFALL, Tags.HORSE, Tags.HPLOW])
|
||||
MWEEP = Sound(0x687A, 'mweep', 'Mweep!', [Tags.BRIEF, Tags.NAVI, Tags.MENUMOVE, Tags.MENUSELECT, Tags.NIGHTFALL, Tags.HPLOW, Tags.HORSE, Tags.HOVERBOOT])
|
||||
NAVI_HELLO = Sound(0x6844, 'navi-hello', 'Navi "Hello!"', [Tags.PAINFUL, Tags.NAVI])
|
||||
NAVI_HEY = Sound(0x685F, 'navi-hey', 'Navi "Hey!"', [Tags.PAINFUL, Tags.HPLOW])
|
||||
NAVI_RANDOM = Sound(0x6843, 'navi-random', 'Navi Random', [Tags.PAINFUL, Tags.HPLOW])
|
||||
NOTIFICATION = Sound(0x4820, 'notification', 'Notification', [Tags.NAVI, Tags.HPLOW])
|
||||
PHANTOM_GANON_LOL = Sound(0x38B0, 'phantom-ganon-laugh', 'Phantom Ganon Laugh', [])
|
||||
PLANT_EXPLODE = Sound(0x284E, 'plant-explode', 'Plant Explode', [])
|
||||
POE = Sound(0x38EC, 'poe', 'Poe', [Tags.PAINFUL, Tags.NAVI])
|
||||
POT_SHATTER = Sound(0x2887, 'shattering-pot', 'Pot Shattering', [Tags.NAVI, Tags.HPLOW])
|
||||
REDEAD_MOAN = Sound(0x38E4, 'redead-moan', 'Redead Moan', [Tags.NIGHTFALL])
|
||||
REDEAD_SCREAM = Sound(0x38E5, 'redead-scream', 'Redead Scream', [Tags.PAINFUL, Tags.NAVI, Tags.HORSE])
|
||||
RIBBIT = Sound(0x28B1, 'ribbit', 'Ribbit', [Tags.NAVI, Tags.HPLOW])
|
||||
RUPEE = Sound(0x4803, 'rupee', 'Rupee', [])
|
||||
RUPEE_SILVER = Sound(0x28E8, 'silver-rupee', 'Rupee (Silver)', [Tags.HPLOW])
|
||||
RUTO_CHILD_CRASH = Sound(0x6860, 'ruto-crash', 'Ruto Crash', [])
|
||||
RUTO_CHILD_EXCITED = Sound(0x6861, 'ruto-excited', 'Ruto Excited', [Tags.PAINFUL])
|
||||
RUTO_CHILD_GIGGLE = Sound(0x6863, 'ruto-giggle', 'Ruto Giggle', [Tags.PAINFUL, Tags.NAVI])
|
||||
RUTO_CHILD_LIFT = Sound(0x6864, 'ruto-lift', 'Ruto Lift', [])
|
||||
RUTO_CHILD_THROWN = Sound(0x6865, 'ruto-thrown', 'Ruto Thrown', [])
|
||||
RUTO_CHILD_WIGGLE = Sound(0x6866, 'ruto-wiggle', 'Ruto Wiggle', [Tags.PAINFUL, Tags.HORSE])
|
||||
SCRUB_NUTS_UP = Sound(0x387C, 'scrub-emerge', 'Scrub Emerge', [])
|
||||
SHABOM_BOUNCE = Sound(0x3948, 'shabom-bounce', 'Shabom Bounce', [Tags.IMMEDIATE])
|
||||
SHABOM_POP = Sound(0x3949, 'shabom-pop', 'Shabom Pop', [Tags.IMMEDIATE, Tags.BRIEF, Tags.HOVERBOOT])
|
||||
SHELLBLADE = Sound(0x3849, 'shellblade', 'Shellblade', [])
|
||||
SKULLTULA = Sound(0x39DA, 'skulltula', 'Skulltula', [Tags.BRIEF, Tags.NAVI])
|
||||
SOFT_BEEP = Sound(0x4804, 'soft-beep', 'Soft Beep', [Tags.NAVI, Tags.HPLOW])
|
||||
SPIKE_TRAP = Sound(0x38E9, 'spike-trap', 'Spike Trap', [Tags.LOOPED, Tags.PAINFUL])
|
||||
SPIT_NUT = Sound(0x387E, 'spit-nut', 'Spit Nut', [Tags.IMMEDIATE, Tags.BRIEF])
|
||||
STALCHILD_ATTACK = Sound(0x3831, 'stalchild-attack', 'Stalchild Attack', [Tags.PAINFUL, Tags.HORSE])
|
||||
STINGER_CRY = Sound(0x39A3, 'stinger-squeak', 'Stinger Squeak', [Tags.PAINFUL])
|
||||
SWITCH = Sound(0x2815, 'switch', 'Switch', [Tags.HPLOW])
|
||||
SWORD_BONK = Sound(0x181A, 'sword-bonk', 'Sword Bonk', [Tags.HPLOW])
|
||||
TALON_CRY = Sound(0x6853, 'talon-cry', 'Talon Cry', [Tags.PAINFUL])
|
||||
TALON_HMM = Sound(0x6852, 'talon-hmm', 'Talon "Hmm"', [])
|
||||
TALON_SNORE = Sound(0x6850, 'talon-snore', 'Talon Snore', [Tags.NIGHTFALL])
|
||||
TALON_WTF = Sound(0x6851, 'talon-wtf', 'Talon Wtf', [])
|
||||
TAMBOURINE = Sound(0x4842, 'tambourine', 'Tambourine', [Tags.QUIET, Tags.NAVI, Tags.HPLOW, Tags.HOVERBOOT])
|
||||
TARGETING_ENEMY = Sound(0x4830, 'target-enemy', 'Target Enemy', [])
|
||||
TARGETING_NEUTRAL = Sound(0x480C, 'target-neutral', 'Target Neutral', [])
|
||||
THUNDER = Sound(0x282E, 'thunder', 'Thunder', [Tags.NIGHTFALL])
|
||||
TIMER = Sound(0x481A, 'timer', 'Timer', [Tags.INC_NE, Tags.NAVI, Tags.HPLOW])
|
||||
TWINROVA_BICKER = Sound(0x39E7, 'twinrova-bicker', 'Twinrova Bicker', [Tags.LOOPED, Tags.PAINFUL])
|
||||
WOLFOS_HOWL = Sound(0x383C, 'wolfos-howl', 'Wolfos Howl', [Tags.PAINFUL])
|
||||
ZELDA_ADULT_GASP = Sound(0x6879, 'adult-zelda-gasp', 'Zelda Gasp (Adult)', [Tags.NAVI, Tags.HPLOW])
|
||||
|
||||
|
||||
# Sound pools
|
||||
standard = [s for s in Sounds if Tags.LOOPED not in s.value.tags]
|
||||
looping = [s for s in Sounds if Tags.LOOPED in s.value.tags]
|
||||
no_painful = [s for s in standard if Tags.PAINFUL not in s.value.tags]
|
||||
navi = [s for s in Sounds if Tags.NAVI in s.value.tags]
|
||||
hp_low = [s for s in Sounds if Tags.HPLOW in s.value.tags]
|
||||
hover_boots = [s for s in Sounds if Tags.HOVERBOOT in s.value.tags]
|
||||
nightfall = [s for s in Sounds if Tags.NIGHTFALL in s.value.tags]
|
||||
menu_select = [s for s in Sounds if Tags.MENUSELECT in s.value.tags]
|
||||
menu_cursor = [s for s in Sounds if Tags.MENUMOVE in s.value.tags]
|
||||
horse_neigh = [s for s in Sounds if Tags.HORSE in s.value.tags]
|
||||
|
||||
|
||||
SoundHook = namedtuple('SoundHook', 'name pool locations')
|
||||
class SoundHooks(Enum):
|
||||
NAVI_OVERWORLD = SoundHook('Navi - Overworld', navi, [0xAE7EF2, 0xC26C7E])
|
||||
NAVI_ENEMY = SoundHook('Navi - Enemy', navi, [0xAE7EC6])
|
||||
HP_LOW = SoundHook('Low Health', hp_low, [0xADBA1A])
|
||||
BOOTS_HOVER = SoundHook('Hover Boots', hover_boots, [0xBDBD8A])
|
||||
NIGHTFALL = SoundHook('Nightfall', nightfall, [0xAD3466, 0xAD7A2E])
|
||||
MENU_SELECT = SoundHook('Menu Select', no_painful + menu_select, [
|
||||
0xBA1BBE, 0xBA23CE, 0xBA2956, 0xBA321A, 0xBA72F6, 0xBA8106, 0xBA82EE,
|
||||
0xBA9DAE, 0xBA9EAE, 0xBA9FD2, 0xBAE6D6])
|
||||
MENU_CURSOR = SoundHook('Menu Cursor', no_painful + menu_cursor, [
|
||||
0xBA165E, 0xBA1C1A, 0xBA2406, 0xBA327E, 0xBA3936, 0xBA77C2, 0xBA7886,
|
||||
0xBA7A06, 0xBA7A6E, 0xBA7AE6, 0xBA7D6A, 0xBA8186, 0xBA822E, 0xBA82A2,
|
||||
0xBAA11E, 0xBAE7C6])
|
||||
HORSE_NEIGH = SoundHook('Horse Neigh', horse_neigh, [
|
||||
0xC18832, 0xC18C32, 0xC19A7E, 0xC19CBE, 0xC1A1F2, 0xC1A3B6, 0xC1B08A,
|
||||
0xC1B556, 0xC1C28A, 0xC1CC36, 0xC1EB4A, 0xC1F18E, 0xC6B136, 0xC6BBA2,
|
||||
0xC1E93A, 0XC6B366, 0XC6B562])
|
||||
|
||||
|
||||
# # Some enemies have a different cutting sound, making this a bit weird
|
||||
# SWORD_SLASH = SoundHook('Sword Slash', standard, [0xAC2942])
|
||||
|
||||
|
||||
def get_patch_dict():
|
||||
return {s.value.keyword: s.value.id for s in Sounds}
|
||||
|
||||
|
||||
def get_hook_pool(sound_hook, earsafeonly = "FALSE"):
|
||||
if earsafeonly == "TRUE":
|
||||
list = [s for s in sound_hook.value.pool if Tags.PAINFUL not in s.value.tags]
|
||||
return list
|
||||
else:
|
||||
return sound_hook.value.pool
|
||||
|
||||
|
||||
def get_setting_choices(sound_hook):
|
||||
pool = sound_hook.value.pool
|
||||
choices = {s.value.keyword: s.value.label for s in sorted(pool, key=lambda s: s.value.label)}
|
||||
result = {
|
||||
'default': 'Default',
|
||||
'completely-random': 'Completely Random',
|
||||
'random-ear-safe': 'Random Ear-Safe',
|
||||
'random-choice': 'Random Choice',
|
||||
'none': 'None',
|
||||
**choices,
|
||||
}
|
||||
return result
|
||||
369
worlds/oot/TextBox.py
Normal file
369
worlds/oot/TextBox.py
Normal file
@@ -0,0 +1,369 @@
|
||||
import worlds.oot.Messages as Messages
|
||||
|
||||
# Least common multiple of all possible character widths. A line wrap must occur when the combined widths of all of the
|
||||
# characters on a line reach this value.
|
||||
NORMAL_LINE_WIDTH = 1801800
|
||||
|
||||
# Attempting to display more lines in a single text box will cause additional lines to bleed past the bottom of the box.
|
||||
LINES_PER_BOX = 4
|
||||
|
||||
# Attempting to display more characters in a single text box will cause buffer overflows. First, visual artifacts will
|
||||
# appear in lower areas of the text box. Eventually, the text box will become uncloseable.
|
||||
MAX_CHARACTERS_PER_BOX = 200
|
||||
|
||||
CONTROL_CHARS = {
|
||||
'LINE_BREAK': ['&', '\x01'],
|
||||
'BOX_BREAK': ['^', '\x04'],
|
||||
'NAME': ['@', '\x0F'],
|
||||
'COLOR': ['#', '\x05\x00'],
|
||||
}
|
||||
TEXT_END = '\x02'
|
||||
|
||||
|
||||
def line_wrap(text, strip_existing_lines=False, strip_existing_boxes=False, replace_control_chars=True):
|
||||
# Replace stand-in characters with their actual control code.
|
||||
if replace_control_chars:
|
||||
for char in CONTROL_CHARS.values():
|
||||
text = text.replace(char[0], char[1])
|
||||
|
||||
# Parse the text into a list of control codes.
|
||||
text_codes = Messages.parse_control_codes(text)
|
||||
|
||||
# Existing line/box break codes to strip.
|
||||
strip_codes = []
|
||||
if strip_existing_boxes:
|
||||
strip_codes.append(0x04)
|
||||
if strip_existing_lines:
|
||||
strip_codes.append(0x01)
|
||||
|
||||
# Replace stripped codes with a space.
|
||||
if strip_codes:
|
||||
index = 0
|
||||
while index < len(text_codes):
|
||||
text_code = text_codes[index]
|
||||
if text_code.code in strip_codes:
|
||||
# Check for existing whitespace near this control code.
|
||||
# If one is found, simply remove this text code.
|
||||
if index > 0 and text_codes[index-1].code == 0x20:
|
||||
text_codes.pop(index)
|
||||
continue
|
||||
if index + 1 < len(text_codes) and text_codes[index+1].code == 0x20:
|
||||
text_codes.pop(index)
|
||||
continue
|
||||
# Replace this text code with a space.
|
||||
text_codes[index] = Messages.Text_Code(0x20, 0)
|
||||
index += 1
|
||||
|
||||
# Split the text codes by current box breaks.
|
||||
boxes = []
|
||||
start_index = 0
|
||||
end_index = 0
|
||||
for text_code in text_codes:
|
||||
end_index += 1
|
||||
if text_code.code == 0x04:
|
||||
boxes.append(text_codes[start_index:end_index])
|
||||
start_index = end_index
|
||||
boxes.append(text_codes[start_index:end_index])
|
||||
|
||||
# Split the boxes into lines and words.
|
||||
processed_boxes = []
|
||||
for box_codes in boxes:
|
||||
line_width = NORMAL_LINE_WIDTH
|
||||
icon_code = None
|
||||
words = []
|
||||
|
||||
# Group the text codes into words.
|
||||
index = 0
|
||||
while index < len(box_codes):
|
||||
text_code = box_codes[index]
|
||||
index += 1
|
||||
|
||||
# Check for an icon code and lower the width of this box if one is found.
|
||||
if text_code.code == 0x13:
|
||||
line_width = 1441440
|
||||
icon_code = text_code
|
||||
|
||||
# Find us a whole word.
|
||||
if text_code.code in [0x01, 0x04, 0x20]:
|
||||
if index > 1:
|
||||
words.append(box_codes[0:index-1])
|
||||
if text_code.code in [0x01, 0x04]:
|
||||
# If we have ran into a line or box break, add it as a "word" as well.
|
||||
words.append([box_codes[index-1]])
|
||||
box_codes = box_codes[index:]
|
||||
index = 0
|
||||
if index > 0 and index == len(box_codes):
|
||||
words.append(box_codes)
|
||||
box_codes = []
|
||||
|
||||
# Arrange our words into lines.
|
||||
lines = []
|
||||
start_index = 0
|
||||
end_index = 0
|
||||
box_count = 1
|
||||
while end_index < len(words):
|
||||
# Our current confirmed line.
|
||||
end_index += 1
|
||||
line = words[start_index:end_index]
|
||||
|
||||
# If this word is a line/box break, trim our line back a word and deal with it later.
|
||||
break_char = False
|
||||
if words[end_index-1][0].code in [0x01, 0x04]:
|
||||
line = words[start_index:end_index-1]
|
||||
break_char = True
|
||||
|
||||
# Check the width of the line after adding one more word.
|
||||
if end_index == len(words) or break_char or calculate_width(words[start_index:end_index+1]) > line_width:
|
||||
if line or lines:
|
||||
lines.append(line)
|
||||
start_index = end_index
|
||||
|
||||
# If we've reached the end of the box, finalize it.
|
||||
if end_index == len(words) or words[end_index-1][0].code == 0x04 or len(lines) == LINES_PER_BOX:
|
||||
# Append the same icon to any wrapped boxes.
|
||||
if icon_code and box_count > 1:
|
||||
lines[0][0] = [icon_code] + lines[0][0]
|
||||
processed_boxes.append(lines)
|
||||
lines = []
|
||||
box_count += 1
|
||||
|
||||
# Construct our final string.
|
||||
# This is a hideous level of list comprehension. Sorry.
|
||||
return '\x04'.join('\x01'.join(' '.join(''.join(code.get_string() for code in word) for word in line) for line in box) for box in processed_boxes)
|
||||
|
||||
|
||||
def calculate_width(words):
|
||||
words_width = 0
|
||||
for word in words:
|
||||
index = 0
|
||||
while index < len(word):
|
||||
character = word[index]
|
||||
index += 1
|
||||
if character.code in Messages.CONTROL_CODES:
|
||||
if character.code == 0x06:
|
||||
words_width += character.data
|
||||
words_width += get_character_width(chr(character.code))
|
||||
spaces_width = get_character_width(' ') * (len(words) - 1)
|
||||
|
||||
return words_width + spaces_width
|
||||
|
||||
|
||||
def get_character_width(character):
|
||||
try:
|
||||
return character_table[character]
|
||||
except KeyError:
|
||||
if ord(character) < 0x20:
|
||||
if character in control_code_width:
|
||||
return sum([character_table[c] for c in control_code_width[character]])
|
||||
else:
|
||||
return 0
|
||||
else:
|
||||
# A sane default with the most common character width
|
||||
return character_table[' ']
|
||||
|
||||
|
||||
control_code_width = {
|
||||
'\x0F': '00000000',
|
||||
'\x16': '00\'00"',
|
||||
'\x17': '00\'00"',
|
||||
'\x18': '00000',
|
||||
'\x19': '100',
|
||||
'\x1D': '00',
|
||||
'\x1E': '00000',
|
||||
'\x1F': '00\'00"',
|
||||
}
|
||||
|
||||
|
||||
# Tediously measured by filling a full line of a gossip stone's text box with one character until it is reasonably full
|
||||
# (with a right margin) and counting how many characters fit. OoT does not appear to use any kerning, but, if it does,
|
||||
# it will only make the characters more space-efficient, so this is an underestimate of the number of letters per line,
|
||||
# at worst. This ensures that we will never bleed text out of the text box while line wrapping.
|
||||
# Larger numbers in the denominator mean more of that character fits on a line; conversely, larger values in this table
|
||||
# mean the character is wider and can't fit as many on one line.
|
||||
character_table = {
|
||||
'\x0F': 655200,
|
||||
'\x16': 292215,
|
||||
'\x17': 292215,
|
||||
'\x18': 300300,
|
||||
'\x19': 145860,
|
||||
'\x1D': 85800,
|
||||
'\x1E': 300300,
|
||||
'\x1F': 265980,
|
||||
'a': 51480, # LINE_WIDTH / 35
|
||||
'b': 51480, # LINE_WIDTH / 35
|
||||
'c': 51480, # LINE_WIDTH / 35
|
||||
'd': 51480, # LINE_WIDTH / 35
|
||||
'e': 51480, # LINE_WIDTH / 35
|
||||
'f': 34650, # LINE_WIDTH / 52
|
||||
'g': 51480, # LINE_WIDTH / 35
|
||||
'h': 51480, # LINE_WIDTH / 35
|
||||
'i': 25740, # LINE_WIDTH / 70
|
||||
'j': 34650, # LINE_WIDTH / 52
|
||||
'k': 51480, # LINE_WIDTH / 35
|
||||
'l': 25740, # LINE_WIDTH / 70
|
||||
'm': 81900, # LINE_WIDTH / 22
|
||||
'n': 51480, # LINE_WIDTH / 35
|
||||
'o': 51480, # LINE_WIDTH / 35
|
||||
'p': 51480, # LINE_WIDTH / 35
|
||||
'q': 51480, # LINE_WIDTH / 35
|
||||
'r': 42900, # LINE_WIDTH / 42
|
||||
's': 51480, # LINE_WIDTH / 35
|
||||
't': 42900, # LINE_WIDTH / 42
|
||||
'u': 51480, # LINE_WIDTH / 35
|
||||
'v': 51480, # LINE_WIDTH / 35
|
||||
'w': 81900, # LINE_WIDTH / 22
|
||||
'x': 51480, # LINE_WIDTH / 35
|
||||
'y': 51480, # LINE_WIDTH / 35
|
||||
'z': 51480, # LINE_WIDTH / 35
|
||||
'A': 81900, # LINE_WIDTH / 22
|
||||
'B': 51480, # LINE_WIDTH / 35
|
||||
'C': 72072, # LINE_WIDTH / 25
|
||||
'D': 72072, # LINE_WIDTH / 25
|
||||
'E': 51480, # LINE_WIDTH / 35
|
||||
'F': 51480, # LINE_WIDTH / 35
|
||||
'G': 81900, # LINE_WIDTH / 22
|
||||
'H': 60060, # LINE_WIDTH / 30
|
||||
'I': 25740, # LINE_WIDTH / 70
|
||||
'J': 51480, # LINE_WIDTH / 35
|
||||
'K': 60060, # LINE_WIDTH / 30
|
||||
'L': 51480, # LINE_WIDTH / 35
|
||||
'M': 81900, # LINE_WIDTH / 22
|
||||
'N': 72072, # LINE_WIDTH / 25
|
||||
'O': 81900, # LINE_WIDTH / 22
|
||||
'P': 51480, # LINE_WIDTH / 35
|
||||
'Q': 81900, # LINE_WIDTH / 22
|
||||
'R': 60060, # LINE_WIDTH / 30
|
||||
'S': 60060, # LINE_WIDTH / 30
|
||||
'T': 51480, # LINE_WIDTH / 35
|
||||
'U': 60060, # LINE_WIDTH / 30
|
||||
'V': 72072, # LINE_WIDTH / 25
|
||||
'W': 100100, # LINE_WIDTH / 18
|
||||
'X': 72072, # LINE_WIDTH / 25
|
||||
'Y': 60060, # LINE_WIDTH / 30
|
||||
'Z': 60060, # LINE_WIDTH / 30
|
||||
' ': 51480, # LINE_WIDTH / 35
|
||||
'1': 25740, # LINE_WIDTH / 70
|
||||
'2': 51480, # LINE_WIDTH / 35
|
||||
'3': 51480, # LINE_WIDTH / 35
|
||||
'4': 60060, # LINE_WIDTH / 30
|
||||
'5': 51480, # LINE_WIDTH / 35
|
||||
'6': 51480, # LINE_WIDTH / 35
|
||||
'7': 51480, # LINE_WIDTH / 35
|
||||
'8': 51480, # LINE_WIDTH / 35
|
||||
'9': 51480, # LINE_WIDTH / 35
|
||||
'0': 60060, # LINE_WIDTH / 30
|
||||
'!': 51480, # LINE_WIDTH / 35
|
||||
'?': 72072, # LINE_WIDTH / 25
|
||||
'\'': 17325, # LINE_WIDTH / 104
|
||||
'"': 34650, # LINE_WIDTH / 52
|
||||
'.': 25740, # LINE_WIDTH / 70
|
||||
',': 25740, # LINE_WIDTH / 70
|
||||
'/': 51480, # LINE_WIDTH / 35
|
||||
'-': 34650, # LINE_WIDTH / 52
|
||||
'_': 51480, # LINE_WIDTH / 35
|
||||
'(': 42900, # LINE_WIDTH / 42
|
||||
')': 42900, # LINE_WIDTH / 42
|
||||
'$': 51480 # LINE_WIDTH / 35
|
||||
}
|
||||
|
||||
# To run tests, enter the following into a python3 REPL:
|
||||
# >>> import Messages
|
||||
# >>> from TextBox import line_wrap_tests
|
||||
# >>> line_wrap_tests()
|
||||
def line_wrap_tests():
|
||||
test_wrap_simple_line()
|
||||
test_honor_forced_line_wraps()
|
||||
test_honor_box_breaks()
|
||||
test_honor_control_characters()
|
||||
test_honor_player_name()
|
||||
test_maintain_multiple_forced_breaks()
|
||||
test_trim_whitespace()
|
||||
test_support_long_words()
|
||||
|
||||
|
||||
def test_wrap_simple_line():
|
||||
words = 'Hello World! Hello World! Hello World!'
|
||||
expected = 'Hello World! Hello World! Hello\x01World!'
|
||||
result = line_wrap(words)
|
||||
|
||||
if result != expected:
|
||||
print('"Wrap Simple Line" test failed: Got ' + result + ', wanted ' + expected)
|
||||
else:
|
||||
print('"Wrap Simple Line" test passed!')
|
||||
|
||||
|
||||
def test_honor_forced_line_wraps():
|
||||
words = 'Hello World! Hello World!&Hello World! Hello World! Hello World!'
|
||||
expected = 'Hello World! Hello World!\x01Hello World! Hello World! Hello\x01World!'
|
||||
result = line_wrap(words)
|
||||
|
||||
if result != expected:
|
||||
print('"Honor Forced Line Wraps" test failed: Got ' + result + ', wanted ' + expected)
|
||||
else:
|
||||
print('"Honor Forced Line Wraps" test passed!')
|
||||
|
||||
|
||||
def test_honor_box_breaks():
|
||||
words = 'Hello World! Hello World!^Hello World! Hello World! Hello World!'
|
||||
expected = 'Hello World! Hello World!\x04Hello World! Hello World! Hello\x01World!'
|
||||
result = line_wrap(words)
|
||||
|
||||
if result != expected:
|
||||
print('"Honor Box Breaks" test failed: Got ' + result + ', wanted ' + expected)
|
||||
else:
|
||||
print('"Honor Box Breaks" test passed!')
|
||||
|
||||
|
||||
def test_honor_control_characters():
|
||||
words = 'Hello World! #Hello# World! Hello World!'
|
||||
expected = 'Hello World! \x05\x00Hello\x05\x00 World! Hello\x01World!'
|
||||
result = line_wrap(words)
|
||||
|
||||
if result != expected:
|
||||
print('"Honor Control Characters" test failed: Got ' + result + ', wanted ' + expected)
|
||||
else:
|
||||
print('"Honor Control Characters" test passed!')
|
||||
|
||||
|
||||
def test_honor_player_name():
|
||||
words = 'Hello @! Hello World! Hello World!'
|
||||
expected = 'Hello \x0F! Hello World!\x01Hello World!'
|
||||
result = line_wrap(words)
|
||||
|
||||
if result != expected:
|
||||
print('"Honor Player Name" test failed: Got ' + result + ', wanted ' + expected)
|
||||
else:
|
||||
print('"Honor Player Name" test passed!')
|
||||
|
||||
|
||||
def test_maintain_multiple_forced_breaks():
|
||||
words = 'Hello World!&&&Hello World!'
|
||||
expected = 'Hello World!\x01\x01\x01Hello World!'
|
||||
result = line_wrap(words)
|
||||
|
||||
if result != expected:
|
||||
print('"Maintain Multiple Forced Breaks" test failed: Got ' + result + ', wanted ' + expected)
|
||||
else:
|
||||
print('"Maintain Multiple Forced Breaks" test passed!')
|
||||
|
||||
|
||||
def test_trim_whitespace():
|
||||
words = 'Hello World! & Hello World!'
|
||||
expected = 'Hello World!\x01Hello World!'
|
||||
result = line_wrap(words)
|
||||
|
||||
if result != expected:
|
||||
print('"Trim Whitespace" test failed: Got ' + result + ', wanted ' + expected)
|
||||
else:
|
||||
print('"Trim Whitespace" test passed!')
|
||||
|
||||
|
||||
def test_support_long_words():
|
||||
words = 'Hello World! WWWWWWWWWWWWWWWWWWWW Hello World!'
|
||||
expected = 'Hello World!\x01WWWWWWWWWWWWWWWWWWWW\x01Hello World!'
|
||||
result = line_wrap(words)
|
||||
|
||||
if result != expected:
|
||||
print('"Support Long Words" test failed: Got ' + result + ', wanted ' + expected)
|
||||
else:
|
||||
print('"Support Long Words" test passed!')
|
||||
99
worlds/oot/Utils.py
Normal file
99
worlds/oot/Utils.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import io, re, json
|
||||
import os, sys
|
||||
import subprocess
|
||||
import Utils
|
||||
from functools import lru_cache
|
||||
|
||||
__version__ = Utils.__version__ + ' f.LUM'
|
||||
|
||||
|
||||
def data_path(*args):
|
||||
return os.path.join(os.path.dirname(__file__), 'data', *args)
|
||||
|
||||
|
||||
@lru_cache(maxsize=13) # Cache Overworld.json and the 12 dungeons
|
||||
def read_json(file_path):
|
||||
json_string = ""
|
||||
with io.open(file_path, 'r') as file:
|
||||
for line in file.readlines():
|
||||
json_string += line.split('#')[0].replace('\n', ' ')
|
||||
json_string = re.sub(' +', ' ', json_string)
|
||||
try:
|
||||
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" + \
|
||||
" ^^\n")
|
||||
|
||||
|
||||
# From the pyinstaller Wiki: https://github.com/pyinstaller/pyinstaller/wiki/Recipe-subprocess
|
||||
# Create a set of arguments which make a ``subprocess.Popen`` (and
|
||||
# variants) call work with or without Pyinstaller, ``--noconsole`` or
|
||||
# not, on Windows and Linux. Typical use::
|
||||
# subprocess.call(['program_to_run', 'arg_1'], **subprocess_args())
|
||||
def subprocess_args(include_stdout=True):
|
||||
# The following is true only on Windows.
|
||||
if hasattr(subprocess, 'STARTUPINFO'):
|
||||
# On Windows, subprocess calls will pop up a command window by default
|
||||
# when run from Pyinstaller with the ``--noconsole`` option. Avoid this
|
||||
# distraction.
|
||||
si = subprocess.STARTUPINFO()
|
||||
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
# Windows doesn't search the path by default. Pass it an environment so
|
||||
# it will.
|
||||
env = os.environ
|
||||
else:
|
||||
si = None
|
||||
env = None
|
||||
|
||||
# ``subprocess.check_output`` doesn't allow specifying ``stdout``::
|
||||
# So, add it only if it's needed.
|
||||
if include_stdout:
|
||||
ret = {'stdout': subprocess.PIPE}
|
||||
else:
|
||||
ret = {}
|
||||
|
||||
# On Windows, running this from the binary produced by Pyinstaller
|
||||
# with the ``--noconsole`` option requires redirecting everything
|
||||
# (stdin, stdout, stderr) to avoid an OSError exception
|
||||
# "[Error 6] the handle is invalid."
|
||||
ret.update({'stdin': subprocess.PIPE,
|
||||
'stderr': subprocess.PIPE,
|
||||
'startupinfo': si,
|
||||
'env': env})
|
||||
return ret
|
||||
|
||||
|
||||
def get_version_bytes(a):
|
||||
version_bytes = [0x00, 0x00, 0x00]
|
||||
if not a:
|
||||
return version_bytes
|
||||
sa = a.replace('v', '').replace(' ', '.').split('.')
|
||||
|
||||
for i in range(0, 3):
|
||||
try:
|
||||
version_byte = int(sa[i])
|
||||
except ValueError:
|
||||
break
|
||||
version_bytes[i] = version_byte
|
||||
|
||||
return version_bytes
|
||||
|
||||
|
||||
def compare_version(a, b):
|
||||
if not a and not b:
|
||||
return 0
|
||||
elif a and not b:
|
||||
return 1
|
||||
elif not a and b:
|
||||
return -1
|
||||
|
||||
sa = get_version_bytes(a)
|
||||
sb = get_version_bytes(b)
|
||||
|
||||
for i in range(0, 3):
|
||||
if sa[i] > sb[i]:
|
||||
return 1
|
||||
if sa[i] < sb[i]:
|
||||
return -1
|
||||
return 0
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user